Files
zeekayandHanzo Dev c16ed9fe26 Go 1.27.1, and the code it can say more simply
Modules declare go 1.27.1 and the build images follow. go fix applied the
modernizers; omitzero is not applied, since it changes what the JSON says.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-17 02:34:35 -07:00

1197 lines
39 KiB
Go

package schemamigrator
import (
"context"
"database/sql"
"errors"
"fmt"
"net/netip"
"slices"
"strings"
"sync"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/hanzo-ds/go"
"github.com/hanzoai/otel-collector/constants"
"go.uber.org/zap"
)
var (
ErrFailedToGetConn = errors.New("failed to get conn")
ErrFailedToGetHostAddrs = errors.New("failed to get host addrs")
ErrFailedToCreateDBs = errors.New("failed to create dbs")
ErrFailedToRunOperation = errors.New("failed to run operation")
ErrFailedToWaitForMutations = errors.New("failed to wait for mutations")
ErrFailedToWaitForDDLQueue = errors.New("failed to wait for DDL queue")
ErrFailedToWaitForDistributionQueue = errors.New("failed to wait for distribution queue")
ErrFailedToRunSquashedMigrations = errors.New("failed to run squashed migrations")
ErrFailedToCreateSchemaMigrationsV2 = errors.New("failed to create schema_migrations_v2 table")
ErrDistributionQueueError = errors.New("distribution_queue has entries with error_count != 0 or is_blocked = 1")
legacyMigrationsTable = "schema_migrations"
O11yLogsDB = "o11y_logs"
O11yMetricsDB = "o11y_metrics"
O11yTracesDB = "o11y_traces"
O11yMetadataDB = "o11y_metadata"
O11yAnalyticsDB = "o11y_analytics"
O11yMeterDB = "o11y_meter"
Databases = []string{O11yTracesDB, O11yMetricsDB, O11yLogsDB, O11yMetadataDB, O11yAnalyticsDB, O11yMeterDB}
InProgressStatus = "in-progress"
FinishedStatus = "finished"
FailedStatus = "failed"
)
type Mutation struct {
Database string `ch:"database"`
Table string `ch:"table"`
MutationID string `ch:"mutation_id"`
Command string `ch:"command"`
CreateTime time.Time `ch:"create_time"`
PartsToDo int64 `ch:"parts_to_do"`
LatestFailReason string `ch:"latest_fail_reason"`
}
type DistributedDDLQueue struct {
Entry string `ch:"entry"`
Cluster string `ch:"cluster"`
Query string `ch:"query"`
QueryCreateTime time.Time `ch:"query_create_time"`
Host string `ch:"host"`
Port uint16 `ch:"port"`
Status string `ch:"status"`
ExceptionCode string `ch:"exception_code"`
}
type SchemaMigrationRecord struct {
MigrationID uint64
UpItems []Operation
DownItems []Operation
}
// MigrationManager is the manager for the schema migrations.
type MigrationManager struct {
// addrs is the list of addresses of the hosts in the cluster.
addrs []string
addrsMux sync.Mutex
conn datastore.Conn
connOpts datastore.Options
conns map[string]datastore.Conn
clusterName string
replicationEnabled bool
logger *zap.Logger
backoff *backoff.ExponentialBackOff
development bool
}
type Option func(*MigrationManager)
// NewMigrationManager creates a new migration manager.
func NewMigrationManager(opts ...Option) (*MigrationManager, error) {
mgr := &MigrationManager{
logger: zap.NewNop(),
// the default backoff is good enough for our use case
// no mutation should be running for more than 15 minutes, if it is, we should fail fast
backoff: backoff.NewExponentialBackOff(),
replicationEnabled: false,
conns: make(map[string]datastore.Conn),
}
for _, opt := range opts {
opt(mgr)
}
if mgr.conn == nil {
return nil, errors.New("conn is required")
}
return mgr, nil
}
func WithClusterName(clusterName string) Option {
return func(mgr *MigrationManager) {
mgr.clusterName = clusterName
}
}
func WithDevelopment(development bool) Option {
return func(mgr *MigrationManager) {
mgr.development = development
}
}
func WithReplicationEnabled(replicationEnabled bool) Option {
return func(mgr *MigrationManager) {
mgr.replicationEnabled = replicationEnabled
}
}
func WithConn(conn datastore.Conn) Option {
return func(mgr *MigrationManager) {
mgr.conn = conn
}
}
func WithConnOptions(opts datastore.Options) Option {
return func(mgr *MigrationManager) {
mgr.connOpts = opts
}
}
func WithLogger(logger *zap.Logger) Option {
return func(mgr *MigrationManager) {
mgr.logger = logger
}
}
func WithBackoff(backoff *backoff.ExponentialBackOff) Option {
return func(mgr *MigrationManager) {
mgr.backoff = backoff
}
}
func (m *MigrationManager) createDBs() error {
for _, db := range Databases {
cmd := fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s ON CLUSTER %s", db, m.clusterName)
if err := m.conn.Exec(context.Background(), cmd); err != nil {
return errors.Join(ErrFailedToCreateDBs, err)
}
}
m.logger.Info("Created databases", zap.Strings("dbs", Databases))
return nil
}
// Bootstrap migrates the schema up for the migrations tables
func (m *MigrationManager) Bootstrap() error {
if err := m.createDBs(); err != nil {
return errors.Join(ErrFailedToCreateDBs, err)
}
m.logger.Info("Creating schema migrations tables")
for _, migration := range V2MigrationTablesLogs {
for _, item := range migration.UpItems {
if err := m.RunOperation(context.Background(), item, migration.MigrationID, O11yLogsDB, true); err != nil {
return errors.Join(ErrFailedToCreateSchemaMigrationsV2, err)
}
}
}
for _, migration := range V2MigrationTablesMetrics {
for _, item := range migration.UpItems {
if err := m.RunOperation(context.Background(), item, migration.MigrationID, O11yMetricsDB, true); err != nil {
return errors.Join(ErrFailedToCreateSchemaMigrationsV2, err)
}
}
}
for _, migration := range V2MigrationTablesTraces {
for _, item := range migration.UpItems {
if err := m.RunOperation(context.Background(), item, migration.MigrationID, O11yTracesDB, true); err != nil {
return errors.Join(ErrFailedToCreateSchemaMigrationsV2, err)
}
}
}
for _, migration := range V2MigrationTablesMetadata {
for _, item := range migration.UpItems {
if err := m.RunOperation(context.Background(), item, migration.MigrationID, O11yMetadataDB, true); err != nil {
return errors.Join(ErrFailedToCreateSchemaMigrationsV2, err)
}
}
}
for _, migration := range V2MigrationTablesAnalytics {
for _, item := range migration.UpItems {
if err := m.RunOperation(context.Background(), item, migration.MigrationID, O11yAnalyticsDB, true); err != nil {
return errors.Join(ErrFailedToCreateSchemaMigrationsV2, err)
}
}
}
for _, migration := range V2MigrationTablesMeter {
for _, item := range migration.UpItems {
if err := m.RunOperation(context.Background(), item, migration.MigrationID, O11yMeterDB, true); err != nil {
return errors.Join(ErrFailedToCreateSchemaMigrationsV2, err)
}
}
}
m.logger.Info("Created schema migrations tables")
return nil
}
// shouldRunSquashed returns true if the legacy migrations table exists and the v2 table does not exist
func (m *MigrationManager) shouldRunSquashed(ctx context.Context, db string) (bool, error) {
var count uint64
err := m.conn.QueryRow(ctx, "SELECT count(*) FROM clusterAllReplicas($1, system.tables) WHERE database = $2 AND name = $3", m.clusterName, db, legacyMigrationsTable).Scan(&count)
if err != nil {
return false, err
}
var countV2 uint64
err = m.conn.QueryRow(ctx, "SELECT count(*) FROM clusterAllReplicas($1, system.tables) WHERE database = $2 AND name = 'schema_migrations_v2'", m.clusterName, db).Scan(&countV2)
if err != nil {
return false, err
}
didMigrateV2 := false
if countV2 > 0 {
// if there are non-zero v2 migrations, we don't need to run the squashed migrations
// fetch count of v2 migrations that are not finished
var countMigrations uint64
err = m.conn.QueryRow(ctx, fmt.Sprintf("SELECT count(*) FROM %s.distributed_schema_migrations_v2", db)).Scan(&countMigrations)
if err != nil {
return false, err
}
didMigrateV2 = countMigrations > 0
}
// we want to run the squashed migrations only if the legacy migrations do not exist
// and v2 migrations did not run
return count == 0 && !didMigrateV2, nil
}
// Returns true if legacy migrations table does not exist
func (m *MigrationManager) ShouldRunSquashedV2(ctx context.Context, db string) (bool, error) {
var count uint64
err := m.conn.QueryRow(ctx, "SELECT count(*) FROM clusterAllReplicas($1, system.tables) WHERE database = $2 AND name = $3", m.clusterName, db, legacyMigrationsTable).Scan(&count)
if err != nil {
return false, err
}
return count == 0, nil
}
func (m *MigrationManager) runCustomRetentionMigrationsForLogs(ctx context.Context) error {
m.logger.Info("Checking if should run squashed migrations for logs")
should, err := m.shouldRunSquashed(ctx, "o11y_logs")
if err != nil {
return err
}
// if the legacy migrations table exists, we don't need to run the custom retention migrations
if !should {
m.logger.Info("skipping custom retention migrations")
return nil
}
m.logger.Info("Running custom retention migrations for logs")
for _, migration := range CustomRetentionLogsMigrations {
for _, item := range migration.UpItems {
if err := m.RunOperation(ctx, item, migration.MigrationID, "o11y_logs", false); err != nil {
return err
}
}
}
m.logger.Info("Custom retention migrations for logs completed")
return nil
}
//nolint:unused
func (m *MigrationManager) runSquashedMigrationsForLogs(ctx context.Context) error {
m.logger.Info("Checking if should run squashed migrations for logs")
should, err := m.shouldRunSquashed(ctx, "o11y_logs")
if err != nil {
return err
}
// if the legacy migrations table exists, we don't need to run the squashed migrations
if !should {
m.logger.Info("skipping squashed migrations")
return nil
}
m.logger.Info("Running squashed migrations for logs")
for _, migration := range SquashedLogsMigrations {
for _, item := range migration.UpItems {
if err := m.RunOperation(ctx, item, migration.MigrationID, "o11y_logs", false); err != nil {
return err
}
}
}
m.logger.Info("Squashed migrations for logs completed")
return nil
}
func (m *MigrationManager) runSquashedMigrationsForMetrics(ctx context.Context) error {
m.logger.Info("Checking if should run squashed migrations for metrics")
should, err := m.shouldRunSquashed(ctx, O11yMetricsDB)
if err != nil {
return err
}
if !should {
m.logger.Info("skipping squashed migrations")
return nil
}
m.logger.Info("Running squashed migrations for metrics")
for _, migration := range SquashedMetricsMigrations {
for _, item := range migration.UpItems {
if err := m.RunOperation(ctx, item, migration.MigrationID, O11yMetricsDB, false); err != nil {
return err
}
}
}
m.logger.Info("Squashed migrations for metrics completed")
return nil
}
func (m *MigrationManager) runSquashedMigrationsForTraces(ctx context.Context) error {
m.logger.Info("Checking if should run squashed migrations for traces")
should, err := m.shouldRunSquashed(ctx, O11yTracesDB)
if err != nil {
return err
}
if !should {
m.logger.Info("skipping squashed migrations")
return nil
}
m.logger.Info("Running squashed migrations for traces")
for _, migration := range SquashedTracesMigrations {
for _, item := range migration.UpItems {
if err := m.RunOperation(ctx, item, migration.MigrationID, O11yTracesDB, false); err != nil {
return err
}
}
}
m.logger.Info("Squashed migrations for traces completed")
return nil
}
func (m *MigrationManager) RunSquashedMigrations(ctx context.Context) error {
m.logger.Info("Running squashed migrations")
if err := m.runCustomRetentionMigrationsForLogs(ctx); err != nil {
return errors.Join(ErrFailedToRunSquashedMigrations, err)
}
if err := m.runSquashedMigrationsForMetrics(ctx); err != nil {
return errors.Join(ErrFailedToRunSquashedMigrations, err)
}
if err := m.runSquashedMigrationsForTraces(ctx); err != nil {
return errors.Join(ErrFailedToRunSquashedMigrations, err)
}
m.logger.Info("Squashed migrations completed")
return nil
}
// HostAddrs returns the addresses of the all hosts in the cluster.
func (m *MigrationManager) HostAddrs() ([]string, error) {
if m.development {
return nil, nil
}
m.addrsMux.Lock()
defer m.addrsMux.Unlock()
if len(m.addrs) != 0 {
return m.addrs, nil
}
hostAddrs := make(map[string]struct{})
query := "SELECT DISTINCT host_address, port FROM system.clusters WHERE host_address NOT IN ['localhost', '127.0.0.1', '::1'] AND cluster = $1"
rows, err := m.conn.Query(context.Background(), query, m.clusterName)
if err != nil {
return nil, errors.Join(ErrFailedToGetHostAddrs, err)
}
defer rows.Close()
for rows.Next() {
var hostAddr string
var port uint16
if err := rows.Scan(&hostAddr, &port); err != nil {
return nil, errors.Join(ErrFailedToGetHostAddrs, err)
}
addr, err := netip.ParseAddr(hostAddr)
if err != nil {
return nil, errors.Join(ErrFailedToGetHostAddrs, err)
}
addrPort := netip.AddrPortFrom(addr, port)
hostAddrs[addrPort.String()] = struct{}{}
}
if len(hostAddrs) != 0 {
// connect to other host and do the same thing
for hostAddr := range hostAddrs {
m.logger.Info("Connecting to new host", zap.String("host", hostAddr))
opts := m.connOpts
opts.Addr = []string{hostAddr}
conn, err := datastore.Open(&opts)
if err != nil {
return nil, errors.Join(ErrFailedToGetConn, err)
}
rows, err := conn.Query(context.Background(), query, m.clusterName)
if err != nil {
return nil, errors.Join(ErrFailedToGetConn, err)
}
defer rows.Close()
for rows.Next() {
var hostAddr string
var port uint16
if err := rows.Scan(&hostAddr, &port); err != nil {
return nil, errors.Join(ErrFailedToGetHostAddrs, err)
}
addr, err := netip.ParseAddr(hostAddr)
if err != nil {
return nil, errors.Join(ErrFailedToGetHostAddrs, err)
}
addrPort := netip.AddrPortFrom(addr, port)
hostAddrs[addrPort.String()] = struct{}{}
}
break
}
}
addrs := make([]string, 0, len(hostAddrs))
for addr := range hostAddrs {
addrs = append(addrs, addr)
}
m.addrs = addrs
return addrs, nil
}
func (m *MigrationManager) getConn(hostAddr string) (datastore.Conn, error) {
m.addrsMux.Lock()
defer m.addrsMux.Unlock()
if conn, ok := m.conns[hostAddr]; ok {
return conn, nil
}
opts := m.connOpts
opts.Addr = []string{hostAddr}
conn, err := datastore.Open(&opts)
if err != nil {
return nil, err
}
m.conns[hostAddr] = conn
return conn, nil
}
func (m *MigrationManager) waitForMutationsOnHost(ctx context.Context, hostAddr string) error {
// reset backoff
m.backoff.Reset()
m.logger.Info("Fetching mutations on host", zap.String("host", hostAddr))
conn, err := m.getConn(hostAddr)
if err != nil {
return err
}
for {
if m.backoff.NextBackOff() == backoff.Stop {
return errors.New("backoff stopped")
}
var mutations []Mutation
if err := conn.Select(ctx, &mutations, "SELECT database, table, command, mutation_id, latest_fail_reason FROM system.mutations WHERE is_done = 0"); err != nil {
return err
}
if len(mutations) != 0 {
m.logger.Info("Waiting for mutations to be completed", zap.Int("count", len(mutations)), zap.String("host", hostAddr))
for _, mutation := range mutations {
m.logger.Info("Mutation details",
zap.String("database", mutation.Database),
zap.String("table", mutation.Table),
zap.String("command", mutation.Command),
zap.String("mutation_id", mutation.MutationID),
zap.String("latest_fail_reason", mutation.LatestFailReason),
)
}
time.Sleep(m.backoff.NextBackOff())
continue
}
m.logger.Info("No mutations found on host", zap.String("host", hostAddr))
break
}
return nil
}
// WaitForRunningMutations waits for all the mutations to be completed on all the hosts in the cluster.
func (m *MigrationManager) WaitForRunningMutations(ctx context.Context) error {
addrs, err := m.HostAddrs()
if err != nil {
return err
}
for _, hostAddr := range addrs {
m.logger.Info("Waiting for mutations on host", zap.String("host", hostAddr))
if err := m.waitForMutationsOnHost(ctx, hostAddr); err != nil {
return errors.Join(ErrFailedToWaitForMutations, err)
}
}
return nil
}
// WaitDistributedDDLQueue waits for all the DDLs to be completed on all the hosts in the cluster.
func (m *MigrationManager) WaitDistributedDDLQueue(ctx context.Context) error {
// reset backoff
m.backoff.Reset()
m.logger.Info("Fetching non-finished DDLs from distributed DDL queue")
for {
if m.backoff.NextBackOff() == backoff.Stop {
return errors.New("backoff stopped")
}
ddlQueue, err := m.getDistributedDDLQueue(ctx)
if err != nil {
return err
}
if len(ddlQueue) != 0 {
m.logger.Info("Waiting for distributed DDL queue to be completed", zap.Int("count", len(ddlQueue)))
for _, ddl := range ddlQueue {
m.logger.Info("DDL details",
zap.String("query", ddl.Query),
zap.String("status", ddl.Status),
zap.String("host", ddl.Host),
zap.String("exception_code", ddl.ExceptionCode),
)
}
time.Sleep(m.backoff.NextBackOff())
continue
}
m.logger.Info("No pending DDLs found in distributed DDL queue")
break
}
return nil
}
func (m *MigrationManager) getDistributedDDLQueue(ctx context.Context) ([]DistributedDDLQueue, error) {
var ddlQueue []DistributedDDLQueue
query := "SELECT entry, cluster, query, host, port, status, exception_code FROM system.distributed_ddl_queue WHERE status != 'Finished'"
// 10 attempts is an arbitrary number. If we don't get the DDL queue after 10 attempts, we give up.
for i := range 10 {
if err := m.conn.Select(ctx, &ddlQueue, query); err != nil {
if exception, ok := err.(*datastore.Exception); ok {
if exception.Code == 999 {
// Datastore DDLWorker is cleaning up entries in the distributed_ddl_queue before we can query it. This leads to the exception:
// code: 999, message: Coordination error: No node, path /datastore/o11y-datastore/task_queue/ddl/query-000000<some 4 digit number>/finished
// It looks like this exception is safe to retry on.
if strings.Contains(exception.Error(), "No node") {
m.logger.Error("A retryable exception was received while fetching distributed DDL queue", zap.Error(err), zap.Int("attempt", i+1))
continue
}
}
}
m.logger.Error("Failed to fetch distributed DDL queue", zap.Error(err), zap.Int("attempt", i+1))
return nil, err
}
// If no exception was thrown, break the loop
break
}
return ddlQueue, nil
}
func (m *MigrationManager) waitForDistributionQueueOnHost(ctx context.Context, conn datastore.Conn, db, table string) error {
errCountQuery := "SELECT count(*) FROM system.distribution_queue WHERE database = $1 AND table = $2 AND (error_count != 0 OR is_blocked = 1)"
var errCount uint64
if err := conn.QueryRow(ctx, errCountQuery, db, table).Scan(&errCount); err != nil {
return errors.Join(ErrFailedToWaitForDistributionQueue, err)
}
if errCount != 0 {
return ErrDistributionQueueError
}
query := "SELECT count(*) FROM system.distribution_queue WHERE database = $1 AND table = $2 AND data_files > 0"
// Should this be configurable and/or higher?
t := time.NewTimer(2 * time.Minute)
defer t.Stop()
minimumInsertsCompletedChan := make(chan struct{})
errChan := make(chan error)
// count for the number of inserts in the queue with non-zero data_files
go func() {
insertsInQueue := 0
for {
var errCount uint64
if err := conn.QueryRow(ctx, errCountQuery, db, table).Scan(&errCount); err != nil {
errChan <- errors.Join(ErrFailedToWaitForDistributionQueue, err)
return
}
if errCount != 0 {
errChan <- ErrDistributionQueueError
return
}
var count uint64
// if the count of inserts in the queue with non-zero data_files is greater than 0, then it counts towards
// one insert, while technically it is more than one insert, we are mainly interested in number of such actions
if err := conn.QueryRow(ctx, query, db, table).Scan(&count); err != nil {
m.logger.Error("Failed to fetch inserts in queue, will retry", zap.Error(err))
continue
}
if count > 0 {
insertsInQueue++
}
if insertsInQueue >= 16 {
minimumInsertsCompletedChan <- struct{}{}
return
}
}
}()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-t.C:
// we waited for graceful period to complete, it might happen that no inserts occur after the migration
// so we can't wait forever
return nil
case err := <-errChan:
return err
case <-minimumInsertsCompletedChan:
return nil
}
}
}
// When dropping a column, we need to make sure that there are no pending items in `distribution_queue`
// for the table. This is because if we drop a column on local table, but there is a pending insert on remote
// table, it will fail.
// There is no deterministic way to find out if there are pending items in `distribution_queue` for a table with
// old schema, so we try to wait for 2 minutes or at least 16 inserts with non-zero `data_files` in the queue
// for the table.
// We need to do this for all hosts in the cluster.
func (m *MigrationManager) WaitForDistributionQueue(ctx context.Context, db, table string) error {
addrs, err := m.HostAddrs()
if err != nil {
return errors.Join(ErrFailedToWaitForDistributionQueue, err)
}
for _, hostAddr := range addrs {
conn, err := m.getConn(hostAddr)
if err != nil {
return errors.Join(ErrFailedToWaitForDistributionQueue, err)
}
if err := m.waitForDistributionQueueOnHost(ctx, conn, db, table); err != nil {
return errors.Join(ErrFailedToWaitForDistributionQueue, err)
}
}
return nil
}
func (m *MigrationManager) shouldRunMigration(db string, migrationID uint64, versions []uint64) bool {
m.logger.Info("Checking if migration should run", zap.String("db", db), zap.Uint64("migration_id", migrationID), zap.Any("versions", versions))
// if versions are provided, we only run the migrations that are in the versions slice
if len(versions) != 0 {
var doesExist bool
if slices.Contains(versions, migrationID) {
doesExist = true
}
if !doesExist {
m.logger.Info("Migration should not run as it is not in the provided versions", zap.Uint64("migration_id", migrationID), zap.Any("versions", versions))
return false
}
}
query := fmt.Sprintf("SELECT * FROM %s.schema_migrations_v2 WHERE migration_id = %d SETTINGS final = 1;", db, migrationID)
m.logger.Info("Fetching migration status", zap.String("query", query))
var migrationSchemaMigrationRecord MigrationSchemaMigrationRecord
if err := m.conn.QueryRow(context.Background(), query).ScanStruct(&migrationSchemaMigrationRecord); err != nil {
if err == sql.ErrNoRows {
m.logger.Info("Migration not run", zap.Uint64("migration_id", migrationID))
return true
}
// this should not happen
m.logger.Error("Failed to fetch migration status", zap.Error(err))
panic(err)
}
m.logger.Info("Migration status", zap.Uint64("migration_id", migrationID), zap.String("status", migrationSchemaMigrationRecord.Status))
if migrationSchemaMigrationRecord.Status != InProgressStatus && migrationSchemaMigrationRecord.Status != FinishedStatus {
m.logger.Info("Migration not run", zap.Uint64("migration_id", migrationID), zap.String("status", migrationSchemaMigrationRecord.Status))
return true
}
return false
}
func (m *MigrationManager) executeSyncOperations(ctx context.Context, operations []Operation, migrationID uint64, db string) error {
for _, item := range operations {
if item.ForceMigrate() || (!item.IsMutation() && item.IsIdempotent() && item.IsLightweight()) {
if err := m.RunOperation(ctx, item, migrationID, db, false); err != nil {
return err
}
}
}
return nil
}
func (m *MigrationManager) IsSync(migration SchemaMigrationRecord) bool {
for _, item := range migration.UpItems {
// if any of the operations is a sync operation, return true
if ok := m.IsSyncOperation(item); ok {
return true
}
}
return false
}
func (m *MigrationManager) IsSyncOperation(item Operation) bool {
return item.ForceMigrate() || (!item.IsMutation() && item.IsIdempotent() && item.IsLightweight())
}
func (m *MigrationManager) IsAsync(migration SchemaMigrationRecord) bool {
for _, item := range migration.UpItems {
// if any of the operations is an async operation, return true
if ok := m.IsAsyncOperation(item); ok {
return true
}
}
return false
}
func (m *MigrationManager) IsAsyncOperation(item Operation) bool {
// If it is a force migrate operation, return false
if item.ForceMigrate() {
return false
}
// If it is a sync operation, return false
if !item.IsMutation() && item.IsIdempotent() && item.IsLightweight() {
return false
}
return true
}
// MigrateUpSync migrates the schema up.
func (m *MigrationManager) MigrateUpSync(ctx context.Context, upVersions []uint64) error {
m.logger.Info("Running migrations up sync")
for _, migration := range TracesMigrations {
if !m.shouldRunMigration(O11yTracesDB, migration.MigrationID, upVersions) {
continue
}
if err := m.executeSyncOperations(ctx, migration.UpItems, migration.MigrationID, O11yTracesDB); err != nil {
return err
}
}
logsMigrations := LogsMigrations
if constants.EnableLogsMigrationsV2 {
logsMigrations = LogsMigrationsV2
}
for _, migration := range logsMigrations {
if !m.shouldRunMigration(O11yLogsDB, migration.MigrationID, upVersions) {
continue
}
if err := m.executeSyncOperations(ctx, migration.UpItems, migration.MigrationID, O11yLogsDB); err != nil {
return err
}
}
for _, migration := range MetricsMigrations {
if !m.shouldRunMigration(O11yMetricsDB, migration.MigrationID, upVersions) {
continue
}
if err := m.executeSyncOperations(ctx, migration.UpItems, migration.MigrationID, O11yMetricsDB); err != nil {
return err
}
}
for _, migration := range MetadataMigrations {
if !m.shouldRunMigration(O11yMetadataDB, migration.MigrationID, upVersions) {
continue
}
if err := m.executeSyncOperations(ctx, migration.UpItems, migration.MigrationID, O11yMetadataDB); err != nil {
return err
}
}
for _, migration := range AnalyticsMigrations {
if !m.shouldRunMigration(O11yAnalyticsDB, migration.MigrationID, upVersions) {
continue
}
if err := m.executeSyncOperations(ctx, migration.UpItems, migration.MigrationID, O11yAnalyticsDB); err != nil {
return err
}
}
for _, migration := range MeterMigrations {
if !m.shouldRunMigration(O11yMeterDB, migration.MigrationID, upVersions) {
continue
}
if err := m.executeSyncOperations(ctx, migration.UpItems, migration.MigrationID, O11yMeterDB); err != nil {
return err
}
}
return nil
}
// MigrateDownSync migrates the schema down.
func (m *MigrationManager) MigrateDownSync(ctx context.Context, downVersions []uint64) error {
m.logger.Info("Running migrations down sync")
for _, migration := range TracesMigrations {
if !m.shouldRunMigration(O11yTracesDB, migration.MigrationID, downVersions) {
continue
}
if err := m.executeSyncOperations(ctx, migration.DownItems, migration.MigrationID, O11yTracesDB); err != nil {
return err
}
}
logsMigrations := LogsMigrations
if constants.EnableLogsMigrationsV2 {
logsMigrations = LogsMigrationsV2
}
for _, migration := range logsMigrations {
if !m.shouldRunMigration(O11yLogsDB, migration.MigrationID, downVersions) {
continue
}
if err := m.executeSyncOperations(ctx, migration.DownItems, migration.MigrationID, O11yLogsDB); err != nil {
return err
}
}
for _, migration := range MetricsMigrations {
if !m.shouldRunMigration(O11yMetricsDB, migration.MigrationID, downVersions) {
continue
}
if err := m.executeSyncOperations(ctx, migration.DownItems, migration.MigrationID, O11yMetricsDB); err != nil {
return err
}
}
for _, migration := range AnalyticsMigrations {
if !m.shouldRunMigration(O11yAnalyticsDB, migration.MigrationID, downVersions) {
continue
}
if err := m.executeSyncOperations(ctx, migration.DownItems, migration.MigrationID, O11yAnalyticsDB); err != nil {
return err
}
}
for _, migration := range MeterMigrations {
if !m.shouldRunMigration(O11yMeterDB, migration.MigrationID, downVersions) {
continue
}
if err := m.executeSyncOperations(ctx, migration.DownItems, migration.MigrationID, O11yMeterDB); err != nil {
return err
}
}
return nil
}
// MigrateUpAsync migrates the schema up.
func (m *MigrationManager) MigrateUpAsync(ctx context.Context, upVersions []uint64) error {
m.logger.Info("Running migrations up async")
for _, migration := range TracesMigrations {
if !m.shouldRunMigration(O11yTracesDB, migration.MigrationID, upVersions) {
continue
}
for _, item := range migration.UpItems {
if item.ForceMigrate() {
m.logger.Info("Skipping force sync operation", zap.Uint64("migration_id", migration.MigrationID))
// skip the force sync operation (should run in sync mode)
continue
}
if !item.IsMutation() && item.IsIdempotent() && item.IsLightweight() {
m.logger.Info("Skipping sync operation", zap.Uint64("migration_id", migration.MigrationID))
// skip the sync operation
continue
}
if item.IsIdempotent() {
if err := m.RunOperation(ctx, item, migration.MigrationID, O11yTracesDB, false); err != nil {
return err
}
}
}
}
for _, migration := range MetricsMigrations {
if !m.shouldRunMigration(O11yMetricsDB, migration.MigrationID, upVersions) {
continue
}
for _, item := range migration.UpItems {
if item.ForceMigrate() {
m.logger.Info("Skipping force sync operation", zap.Uint64("migration_id", migration.MigrationID))
// skip the force sync operation (should run in sync mode)
continue
}
if !item.IsMutation() && item.IsIdempotent() && item.IsLightweight() {
m.logger.Info("Skipping sync operation", zap.Uint64("migration_id", migration.MigrationID))
// skip the sync operation
continue
}
if item.IsIdempotent() {
if err := m.RunOperation(ctx, item, migration.MigrationID, O11yMetricsDB, false); err != nil {
return err
}
}
}
}
logsMigrations := LogsMigrations
if constants.EnableLogsMigrationsV2 {
logsMigrations = LogsMigrationsV2
}
for _, migration := range logsMigrations {
if !m.shouldRunMigration(O11yLogsDB, migration.MigrationID, upVersions) {
continue
}
for _, item := range migration.UpItems {
if item.ForceMigrate() {
m.logger.Info("Skipping force sync operation", zap.Uint64("migration_id", migration.MigrationID))
// skip the force sync operation (should run in sync mode)
continue
}
if !item.IsMutation() && item.IsIdempotent() && item.IsLightweight() {
m.logger.Info("Skipping sync operation", zap.Uint64("migration_id", migration.MigrationID))
// skip the sync operation
continue
}
if item.IsIdempotent() {
if err := m.RunOperation(ctx, item, migration.MigrationID, O11yLogsDB, false); err != nil {
return err
}
}
}
}
return nil
}
// MigrateDownAsync migrates the schema down.
func (m *MigrationManager) MigrateDownAsync(ctx context.Context, downVersions []uint64) error {
m.logger.Info("Running migrations down async")
for _, migration := range TracesMigrations {
if !m.shouldRunMigration(O11yTracesDB, migration.MigrationID, downVersions) {
continue
}
for _, item := range migration.DownItems {
if item.ForceMigrate() {
m.logger.Info("Skipping force sync operation", zap.Uint64("migration_id", migration.MigrationID))
// skip the force sync operation (should run in sync mode)
continue
}
if !item.IsMutation() && item.IsIdempotent() && item.IsLightweight() {
m.logger.Info("Skipping sync operation", zap.Uint64("migration_id", migration.MigrationID))
// skip the sync operation
continue
}
if item.IsIdempotent() {
if err := m.RunOperation(ctx, item, migration.MigrationID, O11yTracesDB, false); err != nil {
return err
}
}
}
}
for _, migration := range MetricsMigrations {
if !m.shouldRunMigration(O11yMetricsDB, migration.MigrationID, downVersions) {
continue
}
for _, item := range migration.DownItems {
if item.ForceMigrate() {
m.logger.Info("Skipping force sync operation", zap.Uint64("migration_id", migration.MigrationID))
// skip the force sync operation (should run in sync mode)
continue
}
if !item.IsMutation() && item.IsIdempotent() && item.IsLightweight() {
m.logger.Info("Skipping sync operation", zap.Uint64("migration_id", migration.MigrationID))
// skip the sync operation
continue
}
if item.IsIdempotent() {
if err := m.RunOperation(ctx, item, migration.MigrationID, O11yMetricsDB, false); err != nil {
return err
}
}
}
}
for _, migration := range LogsMigrations {
if !m.shouldRunMigration(O11yLogsDB, migration.MigrationID, downVersions) {
continue
}
for _, item := range migration.DownItems {
if item.ForceMigrate() {
m.logger.Info("Skipping force sync operation", zap.Uint64("migration_id", migration.MigrationID))
// skip the force sync operation (should run in sync mode)
continue
}
if !item.IsMutation() && item.IsIdempotent() && item.IsLightweight() {
m.logger.Info("Skipping sync operation", zap.Uint64("migration_id", migration.MigrationID))
// skip the sync operation
continue
}
if item.IsIdempotent() {
if err := m.RunOperation(ctx, item, migration.MigrationID, O11yLogsDB, false); err != nil {
return err
}
}
}
}
return nil
}
func (m *MigrationManager) insertMigrationEntry(ctx context.Context, db string, migrationID uint64, status string) error {
query := fmt.Sprintf("INSERT INTO %s.distributed_schema_migrations_v2 (migration_id, status, created_at) VALUES (%d, '%s', '%s')", db, migrationID, status, time.Now().UTC().Format("2006-01-02 15:04:05"))
m.logger.Info("Inserting migration entry", zap.String("query", query))
return m.conn.Exec(ctx, query)
}
func (m *MigrationManager) updateMigrationEntry(ctx context.Context, db string, migrationID uint64, status string, err string) error {
query := fmt.Sprintf("ALTER TABLE %s.schema_migrations_v2 ON CLUSTER %s UPDATE status = $1, error = $2, updated_at = $3 WHERE migration_id = $4", db, m.clusterName)
m.logger.Info("Updating migration entry", zap.String("query", query), zap.String("status", status), zap.String("error", err), zap.Uint64("migration_id", migrationID))
return m.conn.Exec(ctx, query, status, err, time.Now().UTC().Format("2006-01-02 15:04:05"), migrationID)
}
func (m *MigrationManager) RunOperation(ctx context.Context, operation Operation, migrationID uint64, database string, skipStatusUpdate bool) error {
m.logger.Info("Running operation", zap.Uint64("migration_id", migrationID), zap.String("database", database), zap.Bool("skip_status_update", skipStatusUpdate))
start := time.Now()
var sql string
if m.clusterName != "" {
operation = operation.OnCluster(m.clusterName)
}
if m.replicationEnabled {
operation = operation.WithReplication()
}
m.logger.Info("Waiting for running mutations before running the operation")
if err := m.WaitForRunningMutations(ctx); err != nil {
updateErr := m.updateMigrationEntry(ctx, database, migrationID, FailedStatus, err.Error())
if updateErr != nil {
return errors.Join(err, updateErr)
}
return err
}
if err := m.WaitDistributedDDLQueue(ctx); err != nil {
updateErr := m.updateMigrationEntry(ctx, database, migrationID, FailedStatus, err.Error())
if updateErr != nil {
return errors.Join(err, updateErr)
}
return err
}
if shouldWaitForDistributionQueue, database, table := operation.ShouldWaitForDistributionQueue(); shouldWaitForDistributionQueue {
m.logger.Info("Waiting for distribution queue", zap.String("database", database), zap.String("table", table))
if err := m.WaitForDistributionQueue(ctx, database, table); err != nil {
updateErr := m.updateMigrationEntry(ctx, database, migrationID, FailedStatus, err.Error())
if updateErr != nil {
return errors.Join(err, updateErr)
}
return err
}
}
if !skipStatusUpdate {
insertErr := m.insertMigrationEntry(ctx, database, migrationID, InProgressStatus)
if insertErr != nil {
return insertErr
}
}
sql = operation.ToSQL()
m.logger.Info("Running operation", zap.String("sql", sql))
err := m.conn.Exec(ctx, sql)
if err != nil {
updateErr := m.updateMigrationEntry(ctx, database, migrationID, FailedStatus, err.Error())
if updateErr != nil {
return errors.Join(err, updateErr)
}
return err
}
m.logger.Info("Waiting for running mutations after running the operation")
if err := m.WaitForRunningMutations(ctx); err != nil {
updateErr := m.updateMigrationEntry(ctx, database, migrationID, FailedStatus, err.Error())
if updateErr != nil {
return errors.Join(err, updateErr)
}
return err
}
if err := m.WaitDistributedDDLQueue(ctx); err != nil {
updateErr := m.updateMigrationEntry(ctx, database, migrationID, FailedStatus, err.Error())
if updateErr != nil {
return errors.Join(err, updateErr)
}
return err
}
if !skipStatusUpdate {
updateErr := m.updateMigrationEntry(ctx, database, migrationID, FinishedStatus, "")
if updateErr != nil {
return updateErr
}
}
duration := time.Since(start)
m.logger.Info("Operation completed", zap.Uint64("migration_id", migrationID), zap.String("database", database), zap.Duration("duration", duration))
return nil
}
func (m *MigrationManager) RunOperationWithoutUpdate(ctx context.Context, operation Operation, migrationID uint64, database string) error {
m.logger.Info("Running operation", zap.Uint64("migration_id", migrationID), zap.String("database", database))
start := time.Now()
var sql string
if m.clusterName != "" {
operation = operation.OnCluster(m.clusterName)
}
if m.replicationEnabled {
operation = operation.WithReplication()
}
m.logger.Info("Waiting for running mutations before running the operation")
if err := m.WaitForRunningMutations(ctx); err != nil {
return err
}
m.logger.Info("Waiting for distributed DDL queue before running the operation")
if err := m.WaitDistributedDDLQueue(ctx); err != nil {
return err
}
if shouldWaitForDistributionQueue, database, table := operation.ShouldWaitForDistributionQueue(); shouldWaitForDistributionQueue {
m.logger.Info("Waiting for distribution queue", zap.String("database", database), zap.String("table", table))
if err := m.WaitForDistributionQueue(ctx, database, table); err != nil {
return err
}
}
sql = operation.ToSQL()
m.logger.Info("Running operation", zap.String("sql", sql))
err := m.conn.Exec(ctx, sql)
if err != nil {
return err
}
duration := time.Since(start)
m.logger.Info("Operation completed", zap.Uint64("migration_id", migrationID), zap.String("database", database), zap.Duration("duration", duration))
return nil
}
func (m *MigrationManager) InsertMigrationEntry(ctx context.Context, db string, migrationID uint64, status string) error {
query := fmt.Sprintf("INSERT INTO %s.distributed_schema_migrations_v2 (migration_id, status, created_at) VALUES (%d, '%s', '%s')", db, migrationID, status, time.Now().UTC().Format("2006-01-02 15:04:05"))
m.logger.Info("Inserting migration entry", zap.String("query", query))
return m.conn.Exec(ctx, query)
}
func (m *MigrationManager) CheckMigrationStatus(ctx context.Context, db string, migrationID uint64, status string) (bool, error) {
query := fmt.Sprintf("SELECT * FROM %s.distributed_schema_migrations_v2 WHERE migration_id = %d SETTINGS final = 1;", db, migrationID)
m.logger.Info("Checking migration status", zap.String("query", query))
var migrationSchemaMigrationRecord MigrationSchemaMigrationRecord
if err := m.conn.QueryRow(ctx, query).ScanStruct(&migrationSchemaMigrationRecord); err != nil {
if err == sql.ErrNoRows {
return false, nil
}
return false, err
}
return migrationSchemaMigrationRecord.Status == status, nil
}
func (m *MigrationManager) Close() error {
return m.conn.Close()
}