feat: integrate azure storage handling in migration pipeline
Stream transformer output to an Azure blob container when a job declares ToStorage, with per-job prefix and credential configuration.
This commit is contained in:
@@ -1,77 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.ksdemosapps.com/kylesoda/go-migrate/internal/app/config"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
_ "github.com/microsoft/go-mssqldb"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func connectToSqlServer() (*sql.DB, error) {
|
||||
db, err := sql.Open("sqlserver", config.App.SourceDbUrl)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to connect to sqlserver: %w", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
return nil, fmt.Errorf("Unable to ping sqlserver: %w", err)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func connectToPostgres() (*pgxpool.Pool, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
defer cancel()
|
||||
|
||||
pool, err := pgxpool.New(ctx, config.App.TargetDbUrl)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to connect to postgres: %w", err)
|
||||
}
|
||||
|
||||
if err := pool.Ping(ctx); err != nil {
|
||||
pool.Close()
|
||||
return nil, fmt.Errorf("Unable to ping postgres: %w", err)
|
||||
}
|
||||
|
||||
return pool, nil
|
||||
}
|
||||
|
||||
func connectToDatabases() (*sql.DB, *pgxpool.Pool, error) {
|
||||
var sourceDbErr, targetDbErr error
|
||||
var sourceDb *sql.DB
|
||||
var targetDb *pgxpool.Pool
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Go(func() {
|
||||
sourceDb, sourceDbErr = connectToSqlServer()
|
||||
if sourceDbErr != nil {
|
||||
log.Error("Unable to connect to source db: ", sourceDbErr)
|
||||
}
|
||||
})
|
||||
|
||||
wg.Go(func() {
|
||||
targetDb, targetDbErr = connectToPostgres()
|
||||
if targetDbErr != nil {
|
||||
log.Error("Unable to connect to target db: ", targetDbErr)
|
||||
}
|
||||
})
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if sourceDbErr != nil || targetDbErr != nil {
|
||||
return nil, nil, errors.New("Unable to connect to databases")
|
||||
}
|
||||
|
||||
return sourceDb, targetDb, nil
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.ksdemosapps.com/kylesoda/go-migrate/internal/app/config"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@@ -13,5 +14,13 @@ func configureLog() {
|
||||
DisableSorting: false,
|
||||
PadLevelText: true,
|
||||
})
|
||||
log.SetLevel(log.DebugLevel)
|
||||
|
||||
logLevelEnv := config.App.LogLevel
|
||||
logLevel, err := log.ParseLevel(logLevelEnv)
|
||||
if err != nil {
|
||||
log.Warnf("Nivel de log inválido '%s', usando INFO por defecto", logLevelEnv)
|
||||
logLevel = log.InfoLevel
|
||||
}
|
||||
|
||||
log.SetLevel(logLevel)
|
||||
}
|
||||
|
||||
@@ -5,12 +5,13 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.ksdemosapps.com/kylesoda/go-migrate/internal/app/azure"
|
||||
"git.ksdemosapps.com/kylesoda/go-migrate/internal/app/config"
|
||||
"git.ksdemosapps.com/kylesoda/go-migrate/internal/app/db-wrapper"
|
||||
dbwrapper "git.ksdemosapps.com/kylesoda/go-migrate/internal/app/db-wrapper"
|
||||
"git.ksdemosapps.com/kylesoda/go-migrate/internal/app/etl/extractors"
|
||||
"git.ksdemosapps.com/kylesoda/go-migrate/internal/app/etl/loaders"
|
||||
"git.ksdemosapps.com/kylesoda/go-migrate/internal/app/etl/table_analyzers"
|
||||
"git.ksdemosapps.com/kylesoda/go-migrate/internal/app/etl/transformers"
|
||||
"git.ksdemosapps.com/kylesoda/go-migrate/internal/app/models"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
@@ -95,10 +96,10 @@ func processMigrationJobs(
|
||||
targetDb dbwrapper.DbWrapper,
|
||||
jobs []config.Job,
|
||||
maxParallelWorkers int,
|
||||
) []JobResult {
|
||||
) []models.JobResult {
|
||||
if len(jobs) == 0 {
|
||||
log.Info("No migration jobs configured")
|
||||
return []JobResult{}
|
||||
return []models.JobResult{}
|
||||
}
|
||||
|
||||
if maxParallelWorkers <= 0 {
|
||||
@@ -111,15 +112,23 @@ func processMigrationJobs(
|
||||
|
||||
log.Infof("Starting migration with %d parallel worker(s)", maxParallelWorkers)
|
||||
|
||||
chJobResults := make(chan JobResult, len(jobs))
|
||||
chJobResults := make(chan models.JobResult, len(jobs))
|
||||
chJobs := make(chan config.Job, len(jobs))
|
||||
var wgJobs sync.WaitGroup
|
||||
|
||||
sourceTableAnalyzer := table_analyzers.NewMssqlTableAnalyzer(sourceDb)
|
||||
targetTableAnalyzer := table_analyzers.NewPostgresTableAnalyzer(targetDb)
|
||||
extractor := extractors.NewMssqlExtractor(sourceDb)
|
||||
transformer := transformers.NewMssqlTransformer()
|
||||
loader := loaders.NewPostgresLoader(targetDb)
|
||||
loader := loaders.NewGenericLoader(targetDb)
|
||||
|
||||
var azureClient *azure.Client
|
||||
if config.App.AzureStorage.Enabled {
|
||||
var err error
|
||||
azureClient, err = azure.NewClient(config.App.AzureStorage)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create Azure storage client: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
for i := range maxParallelWorkers {
|
||||
wgJobs.Go(func() {
|
||||
@@ -131,9 +140,10 @@ func processMigrationJobs(
|
||||
sourceTableAnalyzer,
|
||||
targetTableAnalyzer,
|
||||
extractor,
|
||||
transformer,
|
||||
azureClient,
|
||||
loader,
|
||||
job,
|
||||
targetDb.GetDialect(),
|
||||
)
|
||||
|
||||
chJobResults <- res
|
||||
@@ -151,7 +161,7 @@ func processMigrationJobs(
|
||||
close(chJobResults)
|
||||
}()
|
||||
|
||||
var finalResults []JobResult
|
||||
var finalResults []models.JobResult
|
||||
for res := range chJobResults {
|
||||
finalResults = append(finalResults, res)
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package main
|
||||
|
||||
import "time"
|
||||
|
||||
type JobResult struct {
|
||||
JobName string
|
||||
StartTime time.Time
|
||||
Duration time.Duration
|
||||
RowsRead int64
|
||||
RowsLoaded int64
|
||||
RowsFailed int64
|
||||
Error error
|
||||
}
|
||||
@@ -2,34 +2,54 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"git.ksdemosapps.com/kylesoda/go-migrate/internal/app/azure"
|
||||
"git.ksdemosapps.com/kylesoda/go-migrate/internal/app/config"
|
||||
"git.ksdemosapps.com/kylesoda/go-migrate/internal/app/custom_errors"
|
||||
dbwrapper "git.ksdemosapps.com/kylesoda/go-migrate/internal/app/db-wrapper"
|
||||
"git.ksdemosapps.com/kylesoda/go-migrate/internal/app/etl"
|
||||
"git.ksdemosapps.com/kylesoda/go-migrate/internal/app/etl/extractors"
|
||||
"git.ksdemosapps.com/kylesoda/go-migrate/internal/app/etl/table_analyzers"
|
||||
"git.ksdemosapps.com/kylesoda/go-migrate/internal/app/etl/transformers"
|
||||
"git.ksdemosapps.com/kylesoda/go-migrate/internal/app/models"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
func buildTruncateQuery(targetDbType, schema, table, truncateMethod string) string {
|
||||
if truncateMethod == "DELETE" {
|
||||
if targetDbType == "postgres" {
|
||||
return fmt.Sprintf(`DELETE FROM "%s"."%s"`, schema, table)
|
||||
}
|
||||
return fmt.Sprintf(`DELETE FROM [%s].[%s]`, schema, table)
|
||||
}
|
||||
|
||||
if targetDbType == "postgres" {
|
||||
return fmt.Sprintf(`TRUNCATE TABLE "%s"."%s"`, schema, table)
|
||||
}
|
||||
return fmt.Sprintf(`TRUNCATE TABLE [%s].[%s]`, schema, table)
|
||||
}
|
||||
|
||||
func processMigrationJob(
|
||||
ctx context.Context,
|
||||
targetDbWrapper dbwrapper.DbWrapper,
|
||||
sourceTableAnalyzer etl.TableAnalyzer,
|
||||
targetTableAnalyzer etl.TableAnalyzer,
|
||||
extractor etl.Extractor,
|
||||
transformer etl.Transformer,
|
||||
azureClient *azure.Client,
|
||||
loader etl.Loader,
|
||||
job config.Job,
|
||||
) JobResult {
|
||||
targetDbType string,
|
||||
) models.JobResult {
|
||||
transformer := transformers.NewMssqlTransformer(job.ToStorage, job.SourceTable, azureClient)
|
||||
localCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
result := JobResult{
|
||||
result := models.JobResult{
|
||||
JobName: job.Name,
|
||||
StartTime: time.Now(),
|
||||
}
|
||||
@@ -65,7 +85,13 @@ func processMigrationJob(
|
||||
return result
|
||||
}
|
||||
|
||||
for _, query := range job.PreSQL {
|
||||
preSqlQueries := job.TargetTable.PreSQL
|
||||
if job.TruncateTarget {
|
||||
truncateQuery := buildTruncateQuery(targetDbType, job.TargetTable.Schema, job.TargetTable.Table, job.TruncateMethod)
|
||||
preSqlQueries = append([]string{truncateQuery}, job.TargetTable.PreSQL...)
|
||||
}
|
||||
|
||||
for _, query := range preSqlQueries {
|
||||
if _, err := targetDbWrapper.Exec(localCtx, query); err != nil {
|
||||
result.Error = err
|
||||
return result
|
||||
@@ -78,6 +104,7 @@ func processMigrationJob(
|
||||
job.SourceTable.TableInfo,
|
||||
job.SourceTable.PrimaryKey,
|
||||
job.RowsPerPartition,
|
||||
job.Range,
|
||||
)
|
||||
if err != nil {
|
||||
log.Error("Unexpected error calculating batch ranges: ", err)
|
||||
@@ -128,8 +155,9 @@ func processMigrationJob(
|
||||
|
||||
for range maxExtractors {
|
||||
wgExtractors.Go(func() {
|
||||
extractor.Exec(
|
||||
extractors.Consume(
|
||||
localCtx,
|
||||
extractor,
|
||||
job.SourceTable,
|
||||
sourceColTypes,
|
||||
job.BatchSize,
|
||||
@@ -213,7 +241,7 @@ func processMigrationJob(
|
||||
cancel()
|
||||
}()
|
||||
|
||||
for _, query := range job.PostSQL {
|
||||
for _, query := range job.TargetTable.PostSQL {
|
||||
if _, err := targetDbWrapper.Exec(localCtx, query); err != nil {
|
||||
result.Error = err
|
||||
return result
|
||||
@@ -233,5 +261,9 @@ func processMigrationJob(
|
||||
result.RowsLoaded = atomic.LoadInt64(&rowsLoaded)
|
||||
result.RowsFailed = atomic.LoadInt64(&rowsFailed)
|
||||
|
||||
if result.RowsRead != result.RowsLoaded {
|
||||
result.Error = fmt.Errorf("Row count mismatch: extracted %d rows but loaded %d rows (failed: %d)", result.RowsRead, result.RowsLoaded, result.RowsFailed)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user