Passed
Pull Request — master (#2447)
by Tolga
03:25
created

pkg/cmd/repair.go   A

Size/Duplication

Total Lines 153
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
cc 13
eloc 89
dl 0
loc 153
rs 10
c 0
b 0
f 0

4 Methods

Rating   Name   Duplication   Size   Complexity  
A cmd.NewRepairCommand 0 11 1
C cmd.repairDatastore 0 73 8
A cmd.maskURL 0 5 2
A cmd.NewRepairDatastoreCommand 0 31 2
1
package cmd
2
3
import (
4
	"context"
5
	"fmt"
6
	"log/slog"
7
	"time"
8
9
	"github.com/spf13/cobra"
10
11
	"github.com/Permify/permify/pkg/database/postgres"
12
)
13
14
const (
15
	repairDatabaseEngine = "database-engine"
16
	repairDatabaseURI    = "database-uri"
17
	repairBatchSize      = "batch-size"
18
	repairDryRun         = "dry-run"
19
	repairVerbose        = "verbose"
20
	repairRetries        = "retries"
21
)
22
23
// NewRepairCommand - Creates new repair command
24
func NewRepairCommand() *cobra.Command {
25
	cmd := &cobra.Command{
26
		Use:   "repair",
27
		Short: "repair PostgreSQL datastore after migration",
28
		Long:  "Repair PostgreSQL datastore to fix XID wraparound issues and transaction ID synchronization problems that can occur after database migration.",
29
		Args:  cobra.NoArgs,
30
	}
31
32
	cmd.AddCommand(NewRepairDatastoreCommand())
33
34
	return cmd
35
}
36
37
// NewRepairDatastoreCommand - Creates new repair datastore command
38
func NewRepairDatastoreCommand() *cobra.Command {
39
	cmd := &cobra.Command{
40
		Use:   "datastore",
41
		Short: "repair PostgreSQL XID counter to prevent wraparound issues",
42
		Long: `Repair PostgreSQL XID counter using a safe approach.
43
44
This command prevents XID wraparound issues by:
45
- Analyzing maximum referenced XIDs in transactions table
46
- Advancing PostgreSQL's XID counter to stay ahead of referenced XIDs
47
- Using safe batch processing to avoid performance impact
48
49
This approach does NOT modify existing data, only advances the XID counter.
50
Use --dry-run to see what would be changed without making actual modifications.`,
51
		RunE: repairDatastore(),
52
		Args: cobra.NoArgs,
53
	}
54
55
	// Add flags
56
	cmd.PersistentFlags().String(repairDatabaseEngine, "postgres", "database engine (only postgres supported)")
57
	cmd.PersistentFlags().String(repairDatabaseURI, "", "database URI (required)")
58
	cmd.PersistentFlags().Int(repairBatchSize, 1000, "batch size for XID advancement")
59
	cmd.PersistentFlags().Bool(repairDryRun, false, "perform a dry run without making changes")
60
	cmd.PersistentFlags().Bool(repairVerbose, true, "enable verbose logging")
61
	cmd.PersistentFlags().Int(repairRetries, 3, "maximum number of retries")
62
63
	// Mark required flags
64
	if err := cmd.MarkPersistentFlagRequired(repairDatabaseURI); err != nil {
65
		panic(fmt.Sprintf("failed to mark flag as required: %v", err))
66
	}
67
68
	return cmd
69
}
70
71
// repairDatastore - permify repair datastore command
72
func repairDatastore() func(cmd *cobra.Command, args []string) error {
73
	return func(cmd *cobra.Command, args []string) error {
74
		// Get database URI and engine
75
		databaseURI, _ := cmd.Flags().GetString(repairDatabaseURI)
76
		databaseEngine, _ := cmd.Flags().GetString(repairDatabaseEngine)
77
78
		// Validate database engine
79
		if databaseEngine != "postgres" {
80
			return fmt.Errorf("only postgres database engine is supported for repair")
81
		}
82
83
		// Create PostgreSQL instance
84
		pg, err := postgres.New(databaseURI)
85
		if err != nil {
86
			return fmt.Errorf("failed to create PostgreSQL instance: %w", err)
0 ignored issues
show
introduced by
unrecognized printf verb 'w'
Loading history...
87
		}
88
		defer pg.Close()
89
90
		// Parse flags directly from cobra
91
		batchSize, _ := cmd.Flags().GetInt(repairBatchSize)
92
		retries, _ := cmd.Flags().GetInt(repairRetries)
93
		dryRun, _ := cmd.Flags().GetBool(repairDryRun)
94
		verbose, _ := cmd.Flags().GetBool(repairVerbose)
95
96
		// Create repair configuration
97
		config := &postgres.RepairConfig{
98
			BatchSize:  batchSize,
99
			MaxRetries: retries,
100
			RetryDelay: 100,
101
			DryRun:     dryRun,
102
			Verbose:    verbose,
103
		}
104
105
		// Perform repair
106
		ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
107
		defer cancel()
108
109
		slog.InfoContext(ctx, "Starting PostgreSQL XID counter repair",
110
			slog.String("database_uri", maskURL(databaseURI)),
111
			slog.Int("batch_size", config.BatchSize),
112
			slog.Bool("dry_run", config.DryRun),
113
			slog.Int("max_retries", config.MaxRetries))
114
115
		start := time.Now()
116
		result, err := pg.Repair(ctx, config)
117
		duration := time.Since(start)
118
119
		if err != nil {
120
			return fmt.Errorf("repair failed: %w", err)
0 ignored issues
show
introduced by
unrecognized printf verb 'w'
Loading history...
121
		}
122
123
		// Print results
124
		slog.InfoContext(ctx, "Repair completed successfully",
125
			slog.Duration("duration", duration),
126
			slog.Int("created_tx_id_fixed", result.CreatedTxIdFixed),
127
			slog.Int("errors", len(result.Errors)))
128
129
		if len(result.Errors) > 0 {
130
			slog.WarnContext(ctx, "Errors encountered during repair")
131
			for i, err := range result.Errors {
132
				slog.WarnContext(ctx, "Repair error",
133
					slog.Int("error_index", i+1),
134
					slog.String("error", err.Error()))
135
			}
136
		}
137
138
		if result.CreatedTxIdFixed > 0 {
139
			slog.InfoContext(ctx, "XID counter repair completed successfully! Advanced XID counter to prevent wraparound issues.")
140
		} else {
141
			slog.InfoContext(ctx, "No XID counter repair needed. PostgreSQL XID counter is already properly positioned.")
142
		}
143
144
		return nil
145
	}
146
}
147
148
// maskURL masks sensitive information in database URL
149
func maskURL(url string) string {
150
	if len(url) < 10 {
151
		return "***"
152
	}
153
	return url[:10] + "***"
154
}
155