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  }