Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Undo breaking schema changes
Browse files Browse the repository at this point in the history
The same schema is used for both revisioned and unrevisioned subtrees. The difference is that we always write a revision of 0 in the unrevisioned case, which still means that there will only be a single entry per subtree. This version uses some magic bytes '🙃norevs🙃' as the prefix of the tree description field to determine whether to disable subtree revision writing. This mechanism isn't necessarily final, but does have some benefits over the previous version: a) the same instance of Trillian can use different modes for different logs, b) no DDL updates required on the schema.
mhutchinson committed Nov 21, 2023
1 parent 1886409 commit 7b71e91
Showing 4 changed files with 56 additions and 18 deletions.
2 changes: 1 addition & 1 deletion storage/mysql/log_storage.go
Original file line number Diff line number Diff line change
@@ -324,7 +324,7 @@ type logTreeTX struct {
func (t *logTreeTX) GetMerkleNodes(ctx context.Context, ids []compact.NodeID) ([]tree.Node, error) {
t.treeTX.mu.Lock()
defer t.treeTX.mu.Unlock()
return t.subtreeCache.GetNodes(ids, t.getSubtreesFunc(ctx))
return t.subtreeCache.GetNodes(ids, t.getSubtreesAtRev(ctx, t.readRev))
}

func (t *logTreeTX) DequeueLeaves(ctx context.Context, limit int, cutoffTime time.Time) ([]*trillian.LogLeaf, error) {
3 changes: 2 additions & 1 deletion storage/mysql/schema/storage.sql
Original file line number Diff line number Diff line change
@@ -40,9 +40,10 @@ CREATE TABLE IF NOT EXISTS Subtree(
TreeId BIGINT NOT NULL,
SubtreeId VARBINARY(255) NOT NULL,
Nodes MEDIUMBLOB NOT NULL,
SubtreeRevision INTEGER NOT NULL,
-- Key columns must be in ASC order in order to benefit from group-by/min-max
-- optimization in MySQL.
PRIMARY KEY(TreeId, SubtreeId),
PRIMARY KEY(TreeId, SubtreeId, SubtreeRevision),
FOREIGN KEY(TreeId) REFERENCES Trees(TreeId) ON DELETE CASCADE
);

67 changes: 52 additions & 15 deletions storage/mysql/tree_storage.go
Original file line number Diff line number Diff line change
@@ -34,7 +34,7 @@ import (

// These statements are fixed
const (
insertSubtreeMultiSQL = `REPLACE INTO Subtree(TreeId, SubtreeId, Nodes) ` + placeholderSQL
insertSubtreeMultiSQL = `REPLACE INTO Subtree(TreeId, SubtreeId, Nodes, SubtreeRevision) ` + placeholderSQL
insertTreeHeadSQL = `INSERT INTO TreeHead(TreeId,TreeHeadTimestamp,TreeSize,RootHash,TreeRevision,RootSignature)
VALUES(?,?,?,?,?,?)`

@@ -52,6 +52,12 @@ const (
AND Subtree.SubtreeRevision = x.MaxRevision
AND Subtree.TreeId = x.TreeId
AND Subtree.TreeId = ?`

selectSubtreeSQLNoRev = `
SELECT SubtreeId, Subtree.Nodes
FROM Subtree
WHERE Subtree.TreeId = ?
AND SubtreeId IN (` + placeholderSQL + `)`
placeholderSQL = "<placeholder>"
)

@@ -133,12 +139,16 @@ func (m *mySQLTreeStorage) getStmt(ctx context.Context, statement string, num in
return s, nil
}

func (m *mySQLTreeStorage) getSubtreeStmt(ctx context.Context, num int) (*sql.Stmt, error) {
return m.getStmt(ctx, selectSubtreeSQL, num, "?", "?")
func (m *mySQLTreeStorage) getSubtreeStmt(ctx context.Context, subtreeRevs bool, num int) (*sql.Stmt, error) {
if subtreeRevs {
return m.getStmt(ctx, selectSubtreeSQL, num, "?", "?")
} else {
return m.getStmt(ctx, selectSubtreeSQLNoRev, num, "?", "?")
}
}

func (m *mySQLTreeStorage) setSubtreeStmt(ctx context.Context, num int) (*sql.Stmt, error) {
return m.getStmt(ctx, insertSubtreeMultiSQL, num, "VALUES(?, ?, ?)", "(?, ?, ?)")
return m.getStmt(ctx, insertSubtreeMultiSQL, num, "VALUES(?, ?, ?, ?)", "(?, ?, ?, ?)")
}

func (m *mySQLTreeStorage) beginTreeTx(ctx context.Context, tree *trillian.Tree, hashSizeBytes int, subtreeCache *cache.SubtreeCache) (treeTX, error) {
@@ -147,6 +157,9 @@ func (m *mySQLTreeStorage) beginTreeTx(ctx context.Context, tree *trillian.Tree,
klog.Warningf("Could not start tree TX: %s", err)
return treeTX{}, err
}
// If the tree description starts with magic constant then we will only write a single
// revision for each subtree tile, which simplifies the reading query.
subtreeRevs := !strings.HasPrefix(tree.Description, "🙃norevs🙃")
return treeTX{
tx: t,
mu: &sync.Mutex{},
@@ -155,6 +168,7 @@ func (m *mySQLTreeStorage) beginTreeTx(ctx context.Context, tree *trillian.Tree,
treeType: tree.TreeType,
hashSizeBytes: hashSizeBytes,
subtreeCache: subtreeCache,
subtreeRevs: subtreeRevs,
writeRevision: -1,
}, nil
}
@@ -170,16 +184,17 @@ type treeTX struct {
hashSizeBytes int
subtreeCache *cache.SubtreeCache
writeRevision int64
subtreeRevs bool
}

func (t *treeTX) getSubtrees(ctx context.Context, ids [][]byte) ([]*storagepb.SubtreeProto, error) {
func (t *treeTX) getSubtrees(ctx context.Context, treeRevision int64, ids [][]byte) ([]*storagepb.SubtreeProto, error) {
klog.V(2).Infof("getSubtrees(len(ids)=%d)", len(ids))
klog.V(4).Infof("getSubtrees(")
if len(ids) == 0 {
return nil, nil
}

tmpl, err := t.ts.getSubtreeStmt(ctx, len(ids))
tmpl, err := t.ts.getSubtreeStmt(ctx, t.subtreeRevs, len(ids))
if err != nil {
return nil, err
}
@@ -190,13 +205,26 @@ func (t *treeTX) getSubtrees(ctx context.Context, ids [][]byte) ([]*storagepb.Su
}
}()

args := make([]interface{}, 0, len(ids)+1)
args = append(args, t.treeID)
var args []interface{}
if t.subtreeRevs {
args = make([]interface{}, 0, len(ids)+3)
// populate args with ids.
for _, id := range ids {
klog.V(4).Infof(" id: %x", id)
args = append(args, id)
}
args = append(args, t.treeID)
args = append(args, treeRevision)
args = append(args, t.treeID)
} else {
args = make([]interface{}, 0, len(ids)+1)
args = append(args, t.treeID)

// populate args with ids.
for _, id := range ids {
klog.V(4).Infof(" id: %x", id)
args = append(args, id)
// populate args with ids.
for _, id := range ids {
klog.V(4).Infof(" id: %x", id)
args = append(args, id)
}
}

rows, err := stx.QueryContext(ctx, args...)
@@ -280,6 +308,12 @@ func (t *treeTX) storeSubtrees(ctx context.Context, subtrees []*storagepb.Subtre
// a really large number of subtrees to store.
args := make([]interface{}, 0, len(subtrees))

subtreeRev := t.writeRevision
if !t.subtreeRevs {
// If we aren't doing subtree revisions then always fill in the subtree
// revision with a constant, such as 0.
subtreeRev = 0
}
for _, s := range subtrees {
s := s
if s.Prefix == nil {
@@ -292,6 +326,7 @@ func (t *treeTX) storeSubtrees(ctx context.Context, subtrees []*storagepb.Subtre
args = append(args, t.treeID)
args = append(args, s.Prefix)
args = append(args, subtreeBytes)
args = append(args, subtreeRev)
}

tmpl, err := t.ts.setSubtreeStmt(ctx, len(subtrees))
@@ -335,16 +370,18 @@ func checkResultOkAndRowCountIs(res sql.Result, err error, count int64) error {
return nil
}

func (t *treeTX) getSubtreesFunc(ctx context.Context) cache.GetSubtreesFunc {
// getSubtreesAtRev returns a GetSubtreesFunc which reads at the passed in rev.
func (t *treeTX) getSubtreesAtRev(ctx context.Context, rev int64) cache.GetSubtreesFunc {
return func(ids [][]byte) ([]*storagepb.SubtreeProto, error) {
return t.getSubtrees(ctx, ids)
return t.getSubtrees(ctx, rev, ids)
}
}

func (t *treeTX) SetMerkleNodes(ctx context.Context, nodes []tree.Node) error {
t.mu.Lock()
defer t.mu.Unlock()
return t.subtreeCache.SetNodes(nodes, t.getSubtreesFunc(ctx))
rev := t.writeRevision - 1
return t.subtreeCache.SetNodes(nodes, t.getSubtreesAtRev(ctx, rev))
}

func (t *treeTX) Commit(ctx context.Context) error {
2 changes: 1 addition & 1 deletion storage/testonly/admin_storage_tester.go
Original file line number Diff line number Diff line change
@@ -37,7 +37,7 @@ var (
TreeState: trillian.TreeState_ACTIVE,
TreeType: trillian.TreeType_LOG,
DisplayName: "Llamas Log",
Description: "Registry of publicly-owned llamas",
Description: "🙃norevs🙃 Registry of publicly-owned llamas",
MaxRootDuration: durationpb.New(0 * time.Millisecond),
}

0 comments on commit 7b71e91

Please sign in to comment.