mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
Switch Serverless from using BoltDB to Postgres as metadata store. Previously, the controller stored all metadata to BoltDB. This implements SQLDB (currently Postgres flavored) as the backing store for metadata. This will allow us to have multiple instances of the controller running for HA, and to easily inspect and repair the contents of the metadata store. Unfortunately, it was not straightforward to keep the BoltDB implementation working alongside the SQL one, so it will be removed in a later patch. Once that's done, the SQL implementation should allow for a number of simplifications of the schemar and balancer interfaces. Database migration is built directly into the application by embedding the migration files and logic from the `soda` command line tool. When connecting to the RDBMS, the app will always attempt to create the necessary database and apply any outstanding migrations. Integration tests truncate all tables upon start, but *not* at the end, so the state of the database can be inspected after integration tests. Had to refactor some of the controller's background tasks to make sure they get properly shut down on controller exit.
138 lines
4 KiB
Go
138 lines
4 KiB
Go
package controller
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/featurebasedb/featurebase/v3/dax"
|
|
"github.com/featurebasedb/featurebase/v3/logger"
|
|
)
|
|
|
|
func (c *Controller) snappingTurtleRoutine(period time.Duration, control chan struct{}, log logger.Logger) error {
|
|
if period == 0 {
|
|
return nil
|
|
}
|
|
ticker := time.NewTicker(period)
|
|
for {
|
|
select {
|
|
case <-c.stopping:
|
|
ticker.Stop()
|
|
log.Debugf("Stopping Snapping Turtle")
|
|
return nil
|
|
case <-ticker.C:
|
|
c.snapAll(log)
|
|
case <-control:
|
|
c.snapAll(log)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Controller) snapAll(log logger.Logger) {
|
|
start := time.Now()
|
|
defer func() {
|
|
log.Printf("full snapshot took: %v", time.Since(start))
|
|
}()
|
|
ctx := context.Background()
|
|
|
|
tx, err := c.Transactor.BeginTx(ctx, false)
|
|
if err != nil {
|
|
log.Printf("Error getting transaction for snapping turtle: %v", err)
|
|
return
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
qdbs, err := c.Schemar.Databases(tx, "")
|
|
if err != nil {
|
|
log.Printf("couldn't get databases: %v", err)
|
|
}
|
|
|
|
for _, qdb := range qdbs {
|
|
c.snapAllForDatabase(tx, qdb.QualifiedID(), log)
|
|
}
|
|
}
|
|
|
|
func (c *Controller) snapAllForDatabase(tx dax.Transaction, qdbid dax.QualifiedDatabaseID, log logger.Logger) {
|
|
log.Debugf("snapAllForDatabase: %s", qdbid)
|
|
computeNodes, err := c.Balancer.CurrentState(tx, dax.RoleTypeCompute, qdbid)
|
|
if err != nil {
|
|
log.Printf("Error getting compute balancer state for snapping turtle: %v", err)
|
|
}
|
|
|
|
// Weird nested loop for snapshotting shard data. The reason for
|
|
// this is to avoid hotspotting each node in turn and spread the
|
|
// snapshotting load across all nodes rather than snapshotting all
|
|
// jobs on one node and then moving onto the next one.
|
|
i := 0
|
|
stillWorking := true
|
|
for stillWorking {
|
|
stillWorking = false
|
|
for _, workerInfo := range computeNodes {
|
|
if len(workerInfo.Jobs) <= i {
|
|
continue
|
|
}
|
|
stillWorking = true
|
|
j, err := decodeShard(workerInfo.Jobs[i])
|
|
if err != nil {
|
|
log.Printf("couldn't decode a shard out of the job: '%s', err: %v", workerInfo.Jobs[i], err)
|
|
}
|
|
if err := c.snapshotShardData(tx, j.t.QualifiedTableID(), j.shardNum()); err != nil {
|
|
log.Printf("Couldn't snapshot table: %s, shard: %d, error: %v", j.t, j.shardNum(), err)
|
|
}
|
|
}
|
|
i++
|
|
}
|
|
|
|
// Get all tables across all orgs/dbs so we can snapshot all keyed
|
|
// fields and look up whether a table is keyed to snapshot its
|
|
// partitions.
|
|
tables, err := c.Schemar.Tables(tx, dax.QualifiedDatabaseID{})
|
|
if err != nil {
|
|
log.Printf("Couldn't get schema for snapshotting keys: %v", err)
|
|
return
|
|
}
|
|
// snapshot keyed fields
|
|
tableMap := make(map[dax.TableKey]*dax.QualifiedTable)
|
|
for _, table := range tables {
|
|
tableMap[table.Key()] = table
|
|
for _, f := range table.Fields {
|
|
if f.StringKeys() && !f.IsPrimaryKey() {
|
|
if err := c.snapshotFieldKeys(tx, table.QualifiedID(), f.Name); err != nil {
|
|
log.Printf("Couldn't snapshot table: %s, field: %s, error: %v", table, f.Name, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get all partition jobs from balancer and snapshot table keys
|
|
// for any partition that goes with a keyed table. Doing the same
|
|
// weird nested loop thing to avoid doing all jobs on one node
|
|
// back to back.
|
|
translateNodes, err := c.Balancer.CurrentState(tx, dax.RoleTypeTranslate, qdbid)
|
|
if err != nil {
|
|
log.Printf("Error getting translate balancer state for snapping turtle: %v", err)
|
|
}
|
|
|
|
i = 0
|
|
stillWorking = true
|
|
for stillWorking {
|
|
stillWorking = false
|
|
for _, workerInfo := range translateNodes {
|
|
if len(workerInfo.Jobs) <= i {
|
|
continue
|
|
}
|
|
stillWorking = true
|
|
j, err := decodePartition(workerInfo.Jobs[i])
|
|
if err != nil {
|
|
table := tableMap[j.table()]
|
|
if table.StringKeys() {
|
|
if err := c.snapshotTableKeys(tx, table.QualifiedID(), j.partitionNum()); err != nil {
|
|
log.Printf("Couldn't snapshot table: %s, partition: %d, error: %v", table, j.partitionNum(), err)
|
|
}
|
|
}
|
|
log.Printf("couldn't decode a partition out of the job: '%s', err: %v", workerInfo.Jobs[i], err)
|
|
}
|
|
}
|
|
i++
|
|
}
|
|
log.Debugf("snapAllForDatabase complete: %s", qdbid)
|
|
}
|