github.com/google/trillian-examples@v0.0.0-20240520080811-0d40d35cef0e/serverless/cmd/clone2serverless/main.go (about) 1 // Copyright 2023 Google LLC. All Rights Reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // clone2serverless is a one-shot tool that creates a tile-based (serverless) 16 // log on disk from the contents of a cloned DB. 17 package main 18 19 import ( 20 "context" 21 "flag" 22 "fmt" 23 "os" 24 "sync" 25 "time" 26 27 "github.com/golang/glog" 28 "github.com/google/trillian-examples/clone/logdb" 29 "github.com/google/trillian-examples/serverless/cmd/clone2serverless/internal/storage/fs" 30 fmtlog "github.com/transparency-dev/formats/log" 31 "github.com/transparency-dev/merkle/rfc6962" 32 "github.com/transparency-dev/serverless-log/pkg/log" 33 "golang.org/x/mod/sumdb/note" 34 35 _ "github.com/go-sql-driver/mysql" 36 ) 37 38 var ( 39 mysqlURI = flag.String("mysql_uri", "", "URL of a MySQL database to clone the log into. The DB should contain only one log.") 40 outputRoot = flag.String("output_root", "", "File path to the directory that the tile-based log will be created in. If this directory exists then it must contain a valid tile-based log. If it doesn't exist it will be created.") 41 vkey = flag.String("log_vkey", "", "Verifier key for the log checkpoint signature.") 42 origin = flag.String("log_origin", "", "Expected Origin for the log checkpoints.") 43 ) 44 45 func main() { 46 flag.Parse() 47 48 if len(*mysqlURI) == 0 { 49 glog.Exit("Missing required parameter 'mysql_uri'") 50 } 51 if len(*origin) == 0 { 52 glog.Exit("Missing required parameter 'log_origin'") 53 } 54 if len(*vkey) == 0 { 55 glog.Exit("Missing required parameter 'log_vkey'") 56 } 57 if len(*outputRoot) == 0 { 58 glog.Exit("Missing required parameter 'output_root'") 59 } 60 61 db, err := logdb.NewDatabase(*mysqlURI) 62 if err != nil { 63 glog.Exitf("Failed to connect to database: %v", err) 64 } 65 logVerifier, err := note.NewVerifier(*vkey) 66 if err != nil { 67 glog.Exitf("Failed to instantiate Verifier: %q", err) 68 } 69 70 ctx := context.Background() 71 toSize, inputCheckpointRaw, _, err := db.GetLatestCheckpoint(ctx) 72 if err != nil { 73 glog.Exitf("Failed to get latest checkpoint from clone DB: %v", err) 74 } 75 logRoot, fromSize, err := initStorage(logVerifier, *outputRoot) 76 if err != nil { 77 glog.Exitf("Could not initialize tile-based storage: %v", err) 78 } 79 80 // Set up a background thread to report progress (as it can take a long time). 81 rCtx, rCtxCancel := context.WithCancel(ctx) 82 defer rCtxCancel() 83 r := reporter{ 84 lastReported: fromSize, 85 lastReport: time.Now(), 86 treeSize: toSize, 87 } 88 go func() { 89 ticker := time.NewTicker(5 * time.Second) 90 defer ticker.Stop() 91 for { 92 select { 93 case <-ticker.C: 94 r.report() 95 case <-rCtx.Done(): 96 return 97 } 98 } 99 }() 100 101 results := make(chan logdb.StreamResult, 16) 102 glog.V(1).Infof("Sequencing leaves [%d, %d)", fromSize, toSize) 103 go db.StreamLeaves(ctx, fromSize, toSize, results) 104 105 index := fromSize 106 for result := range results { 107 if result.Err != nil { 108 glog.Exitf("Failed fetching leaves from DB: %v", result.Err) 109 } 110 workDone := r.trackWork(index) 111 err := logRoot.Assign(ctx, index, result.Leaf) 112 if err != nil { 113 glog.Exitf("Failed to sequence leaves: %v", err) 114 } 115 workDone() 116 index++ 117 } 118 rCtxCancel() 119 120 glog.V(1).Infof("Integrating leaves [%d, %d)", fromSize, toSize) 121 if _, err := log.Integrate(ctx, fromSize, logRoot, rfc6962.DefaultHasher); err != nil { 122 glog.Exitf("Failed to integrate leaves: %v", err) 123 } 124 glog.V(1).Info("Writing checkpoint") 125 if err := logRoot.WriteCheckpoint(ctx, inputCheckpointRaw); err != nil { 126 glog.Exitf("Failed to write checkpoint: %v", err) 127 } 128 glog.V(1).Infof("Successfully created tlog with %d leaves at %q", toSize, *outputRoot) 129 } 130 131 // initStorage loads or creates the tile-based log storage in the given output directory 132 // and returns the storage, along with the tree size currently persisted. 133 func initStorage(logVerifier note.Verifier, outputRoot string) (*fs.Storage, uint64, error) { 134 var logRoot *fs.Storage 135 var fromSize uint64 136 if fstat, err := os.Stat(outputRoot); os.IsNotExist(err) { 137 glog.Infof("No directory exists at %q; creating new tile-based log filesystem", outputRoot) 138 logRoot, err = fs.Create(outputRoot) 139 if err != nil { 140 return nil, 0, fmt.Errorf("failed to create log root filesystem: %v", err) 141 } 142 glog.Infof("Created new log root filesystem at %q", outputRoot) 143 } else if err != nil { 144 return nil, 0, fmt.Errorf("failed to stat %q: %v", outputRoot, err) 145 } else if !fstat.IsDir() { 146 return nil, 0, fmt.Errorf("output root is not a directory: %v", outputRoot) 147 } else { 148 glog.Infof("Reading existing tile-based log from %q", outputRoot) 149 fsCheckpointRaw, err := fs.ReadCheckpoint(outputRoot) 150 if err != nil { 151 return nil, 0, fmt.Errorf("failed to read checkpoint from log root filesystem: %v", err) 152 } 153 fsCheckpoint, _, _, err := fmtlog.ParseCheckpoint(fsCheckpointRaw, *origin, logVerifier) 154 if err != nil { 155 return nil, 0, fmt.Errorf("failed to open Checkpoint: %q", err) 156 } 157 logRoot, err = fs.Load(outputRoot, fromSize) 158 if err != nil { 159 return nil, 0, fmt.Errorf("failed to load log root filesystem: %v", err) 160 } 161 fromSize = fsCheckpoint.Size 162 } 163 return logRoot, fromSize, nil 164 } 165 166 // reporter is copied from clone/internal/cloner/clone.go. 167 type reporter struct { 168 // treeSize is fixed for the lifetime of the reporter. 169 treeSize uint64 170 171 // Fields read/written only in report() 172 lastReport time.Time 173 lastReported uint64 174 175 // Fields shared across multiple threads, protected by workedMutex 176 lastWorked uint64 177 epochWorked time.Duration 178 workedMutex sync.Mutex 179 } 180 181 func (r *reporter) report() { 182 lastWorked, epochWorked := func() (uint64, time.Duration) { 183 r.workedMutex.Lock() 184 defer r.workedMutex.Unlock() 185 lw, ew := r.lastWorked, r.epochWorked 186 r.epochWorked = 0 187 return lw, ew 188 }() 189 190 elapsed := time.Since(r.lastReport) 191 workRatio := epochWorked.Seconds() / elapsed.Seconds() 192 remaining := r.treeSize - r.lastReported - 1 193 rate := float64(lastWorked-r.lastReported) / elapsed.Seconds() 194 eta := time.Duration(float64(remaining)/rate) * time.Second 195 glog.Infof("%.1f leaves/s, last leaf=%d (remaining: %d, ETA: %s), time working=%.1f%%", rate, r.lastReported, remaining, eta, 100*workRatio) 196 197 r.lastReport = time.Now() 198 r.lastReported = r.lastWorked 199 } 200 201 func (r *reporter) trackWork(index uint64) func() { 202 start := time.Now() 203 204 return func() { 205 end := time.Now() 206 r.workedMutex.Lock() 207 defer r.workedMutex.Unlock() 208 r.lastWorked = index 209 r.epochWorked += end.Sub(start) 210 } 211 }