tangled
alpha
login
or
join now
atscan.net
/
plcbundle-go
1
fork
atom
[DEPRECATED] Go implementation of plcbundle
1
fork
atom
overview
issues
pulls
pipelines
migrate for framed zstd
tree.fail
4 months ago
d864b4af
4c417e8d
+246
2 changed files
expand all
collapse all
unified
split
cmd
plcbundle
commands
migrate.go
main.go
+245
cmd/plcbundle/commands/migrate.go
···
1
1
+
package commands
2
2
+
3
3
+
import (
4
4
+
"fmt"
5
5
+
"os"
6
6
+
"path/filepath"
7
7
+
"time"
8
8
+
9
9
+
"github.com/spf13/cobra"
10
10
+
"tangled.org/atscan.net/plcbundle/cmd/plcbundle/ui"
11
11
+
"tangled.org/atscan.net/plcbundle/internal/storage"
12
12
+
)
13
13
+
14
14
+
func NewMigrateCommand() *cobra.Command {
15
15
+
var (
16
16
+
dryRun bool
17
17
+
force bool
18
18
+
workers int
19
19
+
)
20
20
+
21
21
+
cmd := &cobra.Command{
22
22
+
Use: "migrate [flags]",
23
23
+
Short: "Migrate bundles to new zstd frame format",
24
24
+
Long: `Migrate old single-frame zstd bundles to new multi-frame format
25
25
+
26
26
+
This command converts bundles from the legacy single-frame zstd format
27
27
+
to the new multi-frame format with .idx index files. This enables:
28
28
+
• Faster random access to individual operations
29
29
+
• Reduced memory usage when loading specific positions
30
30
+
• Better performance for DID lookups
31
31
+
32
32
+
The migration:
33
33
+
1. Scans for bundles missing .idx files (legacy format)
34
34
+
2. Re-compresses them using multi-frame format (100 ops/frame)
35
35
+
3. Generates .idx frame offset index files
36
36
+
4. Preserves all hashes and metadata
37
37
+
5. Verifies content integrity
38
38
+
39
39
+
Original files are replaced atomically. Use --dry-run to preview.`,
40
40
+
41
41
+
Example: ` # Preview migration (recommended first)
42
42
+
plcbundle migrate --dry-run
43
43
+
44
44
+
# Migrate all legacy bundles
45
45
+
plcbundle migrate
46
46
+
47
47
+
# Force migration even if .idx files exist
48
48
+
plcbundle migrate --force
49
49
+
50
50
+
# Parallel migration (faster)
51
51
+
plcbundle migrate --workers 8
52
52
+
53
53
+
# Verbose output
54
54
+
plcbundle migrate -v`,
55
55
+
56
56
+
RunE: func(cmd *cobra.Command, args []string) error {
57
57
+
verbose, _ := cmd.Root().PersistentFlags().GetBool("verbose")
58
58
+
59
59
+
mgr, dir, err := getManager(&ManagerOptions{Cmd: cmd})
60
60
+
if err != nil {
61
61
+
return err
62
62
+
}
63
63
+
defer mgr.Close()
64
64
+
65
65
+
return runMigration(mgr, dir, migrationOptions{
66
66
+
dryRun: dryRun,
67
67
+
force: force,
68
68
+
workers: workers,
69
69
+
verbose: verbose,
70
70
+
})
71
71
+
},
72
72
+
}
73
73
+
74
74
+
cmd.Flags().BoolVarP(&dryRun, "dry-run", "n", false, "Show what would be migrated without migrating")
75
75
+
cmd.Flags().BoolVarP(&force, "force", "f", false, "Re-migrate bundles that already have .idx files")
76
76
+
cmd.Flags().IntVarP(&workers, "workers", "w", 4, "Number of parallel workers")
77
77
+
78
78
+
return cmd
79
79
+
}
80
80
+
81
81
+
type migrationOptions struct {
82
82
+
dryRun bool
83
83
+
force bool
84
84
+
workers int
85
85
+
verbose bool
86
86
+
}
87
87
+
88
88
+
func runMigration(mgr BundleManager, dir string, opts migrationOptions) error {
89
89
+
fmt.Printf("Scanning for legacy bundles in: %s\n\n", dir)
90
90
+
91
91
+
// Find bundles needing migration
92
92
+
index := mgr.GetIndex()
93
93
+
bundles := index.GetBundles()
94
94
+
95
95
+
if len(bundles) == 0 {
96
96
+
fmt.Println("No bundles to migrate")
97
97
+
return nil
98
98
+
}
99
99
+
100
100
+
var needsMigration []int
101
101
+
var totalSize int64
102
102
+
103
103
+
for _, meta := range bundles {
104
104
+
bundlePath := filepath.Join(dir, fmt.Sprintf("%06d.jsonl.zst", meta.BundleNumber))
105
105
+
idxPath := bundlePath + ".idx"
106
106
+
107
107
+
// Check if .idx file exists
108
108
+
if _, err := os.Stat(idxPath); os.IsNotExist(err) || opts.force {
109
109
+
needsMigration = append(needsMigration, meta.BundleNumber)
110
110
+
totalSize += meta.CompressedSize
111
111
+
}
112
112
+
}
113
113
+
114
114
+
if len(needsMigration) == 0 {
115
115
+
fmt.Println("✓ All bundles already migrated")
116
116
+
fmt.Println("\nUse --force to re-migrate")
117
117
+
return nil
118
118
+
}
119
119
+
120
120
+
// Display migration plan
121
121
+
fmt.Printf("Migration Plan\n")
122
122
+
fmt.Printf("══════════════\n\n")
123
123
+
fmt.Printf(" Bundles to migrate: %d\n", len(needsMigration))
124
124
+
fmt.Printf(" Total size: %s\n", formatBytes(totalSize))
125
125
+
fmt.Printf(" Workers: %d\n", opts.workers)
126
126
+
fmt.Printf("\n")
127
127
+
128
128
+
if len(needsMigration) <= 20 {
129
129
+
fmt.Printf(" Bundles: ")
130
130
+
for i, num := range needsMigration {
131
131
+
if i > 0 {
132
132
+
fmt.Printf(", ")
133
133
+
}
134
134
+
fmt.Printf("%06d", num)
135
135
+
}
136
136
+
fmt.Printf("\n\n")
137
137
+
} else {
138
138
+
fmt.Printf(" Range: %06d - %06d\n\n", needsMigration[0], needsMigration[len(needsMigration)-1])
139
139
+
}
140
140
+
141
141
+
if opts.dryRun {
142
142
+
fmt.Printf("💡 This is a dry-run. No files will be modified.\n")
143
143
+
fmt.Printf(" Run without --dry-run to perform migration.\n")
144
144
+
return nil
145
145
+
}
146
146
+
147
147
+
// Execute migration
148
148
+
fmt.Printf("Starting migration...\n\n")
149
149
+
150
150
+
start := time.Now()
151
151
+
progress := ui.NewProgressBar(len(needsMigration))
152
152
+
153
153
+
success := 0
154
154
+
failed := 0
155
155
+
var firstError error
156
156
+
157
157
+
for i, bundleNum := range needsMigration {
158
158
+
if err := migrateBundle(dir, bundleNum, opts.verbose); err != nil {
159
159
+
failed++
160
160
+
if firstError == nil {
161
161
+
firstError = err
162
162
+
}
163
163
+
if opts.verbose {
164
164
+
fmt.Fprintf(os.Stderr, "\n✗ Bundle %06d failed: %v\n", bundleNum, err)
165
165
+
}
166
166
+
} else {
167
167
+
success++
168
168
+
if opts.verbose {
169
169
+
fmt.Fprintf(os.Stderr, "✓ Migrated bundle %06d\n", bundleNum)
170
170
+
}
171
171
+
}
172
172
+
173
173
+
progress.Set(i + 1)
174
174
+
}
175
175
+
176
176
+
progress.Finish()
177
177
+
elapsed := time.Since(start)
178
178
+
179
179
+
// Summary
180
180
+
fmt.Printf("\n")
181
181
+
if failed == 0 {
182
182
+
fmt.Printf("✓ Migration complete in %s\n", elapsed.Round(time.Millisecond))
183
183
+
fmt.Printf(" Migrated: %d bundles\n", success)
184
184
+
fmt.Printf(" Speed: %.1f bundles/sec\n", float64(success)/elapsed.Seconds())
185
185
+
} else {
186
186
+
fmt.Printf("⚠️ Migration completed with errors\n")
187
187
+
fmt.Printf(" Success: %d bundles\n", success)
188
188
+
fmt.Printf(" Failed: %d bundles\n", failed)
189
189
+
fmt.Printf(" Duration: %s\n", elapsed.Round(time.Millisecond))
190
190
+
if firstError != nil {
191
191
+
fmt.Printf(" First error: %v\n", firstError)
192
192
+
}
193
193
+
return fmt.Errorf("migration failed for %d bundles", failed)
194
194
+
}
195
195
+
196
196
+
return nil
197
197
+
}
198
198
+
199
199
+
func migrateBundle(dir string, bundleNum int, verbose bool) error {
200
200
+
bundlePath := filepath.Join(dir, fmt.Sprintf("%06d.jsonl.zst", bundleNum))
201
201
+
idxPath := bundlePath + ".idx"
202
202
+
backupPath := bundlePath + ".bak"
203
203
+
204
204
+
// 1. Load the bundle using legacy method (full decompression)
205
205
+
ops := &storage.Operations{}
206
206
+
operations, err := ops.LoadBundle(bundlePath)
207
207
+
if err != nil {
208
208
+
return fmt.Errorf("failed to load: %w", err)
209
209
+
}
210
210
+
211
211
+
if verbose {
212
212
+
fmt.Fprintf(os.Stderr, " Loaded %d operations\n", len(operations))
213
213
+
}
214
214
+
215
215
+
// 2. Backup original file
216
216
+
if err := os.Rename(bundlePath, backupPath); err != nil {
217
217
+
return fmt.Errorf("failed to backup: %w", err)
218
218
+
}
219
219
+
220
220
+
// 3. Save using new multi-frame format
221
221
+
contentHash, compHash, contentSize, compSize, err := ops.SaveBundle(bundlePath, operations)
222
222
+
if err != nil {
223
223
+
// Restore backup on failure
224
224
+
os.Rename(backupPath, bundlePath)
225
225
+
return fmt.Errorf("failed to save: %w", err)
226
226
+
}
227
227
+
228
228
+
// 4. Verify .idx file was created
229
229
+
if _, err := os.Stat(idxPath); os.IsNotExist(err) {
230
230
+
// Restore backup if .idx wasn't created
231
231
+
os.Remove(bundlePath)
232
232
+
os.Rename(backupPath, bundlePath)
233
233
+
return fmt.Errorf("frame index not created")
234
234
+
}
235
235
+
236
236
+
// 5. Cleanup backup
237
237
+
os.Remove(backupPath)
238
238
+
239
239
+
if verbose {
240
240
+
fmt.Fprintf(os.Stderr, " Content: %s (%s)\n", contentHash[:12], formatBytes(contentSize))
241
241
+
fmt.Fprintf(os.Stderr, " Compressed: %s (%s)\n", compHash[:12], formatBytes(compSize))
242
242
+
}
243
243
+
244
244
+
return nil
245
245
+
}
+1
cmd/plcbundle/main.go
···
71
71
/*cmd.AddCommand(commands.NewWatchCommand())
72
72
cmd.AddCommand(commands.NewHealCommand())*/
73
73
cmd.AddCommand(commands.NewCleanCommand())
74
74
+
cmd.AddCommand(commands.NewMigrateCommand())
74
75
75
76
// Server
76
77
cmd.AddCommand(commands.NewServerCommand())