← back to Cli Printing Press
fix(cli): force sync concurrency=1 under PRINTING_PRESS_VERIFY to avoid SQLITE_BUSY (#1441)
b8b1b6a4d4b6a8f037b8dc3f78625a1b676ea402 · 2026-05-15 10:46:55 -0700 · Trevin Chow
* fix(cli): force sync concurrency=1 under PRINTING_PRESS_VERIFY to avoid SQLITE_BUSY
Under PRINTING_PRESS_VERIFY=1 (mock/dry-run), the generated sync command's
worker pool used to race on SQLite writes once network latency disappeared,
tripping SQLITE_BUSY despite _busy_timeout. validate-narrative --full-examples
failed any quickstart or recipe that exercised sync on a CLI with more than a
handful of resources.
Force concurrency to 1 only when cliutil.IsVerifyEnv() returns true; real-world
sync keeps the parallel worker pool unchanged.
Touches sync.go.tmpl and graphql_sync.go.tmpl plus generator regression tests
that pin the import, the IsVerifyEnv() guard, and the after-default ordering.
Three sync.go golden fixtures refresh accordingly.
Closes #1205
* fix(cli): correct misleading ordering rationale in sync verify-env test helper
Greptile flagged the comment claim that placing the verify-env block before
the default-resolution block "would reset the pin on every call." That's
wrong — `1 < 1` is false, so the default block is a no-op once the verify
block sets concurrency to 1, regardless of ordering.
Reframe the assertion as a defensive guard against template churn rather
than a correctness invariant, and remove the false rationale from the
helper's docstring and assertion message.
Files touched
M internal/generator/generator_test.goM internal/generator/templates/graphql_sync.go.tmplM internal/generator/templates/sync.go.tmplM testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/sync.goM testdata/golden/expected/generate-sync-walker-api/sync-walker-golden/internal/cli/sync.goM testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/sync.go
Diff
commit b8b1b6a4d4b6a8f037b8dc3f78625a1b676ea402
Author: Trevin Chow <trevin@trevinchow.com>
Date: Fri May 15 10:46:55 2026 -0700
fix(cli): force sync concurrency=1 under PRINTING_PRESS_VERIFY to avoid SQLITE_BUSY (#1441)
* fix(cli): force sync concurrency=1 under PRINTING_PRESS_VERIFY to avoid SQLITE_BUSY
Under PRINTING_PRESS_VERIFY=1 (mock/dry-run), the generated sync command's
worker pool used to race on SQLite writes once network latency disappeared,
tripping SQLITE_BUSY despite _busy_timeout. validate-narrative --full-examples
failed any quickstart or recipe that exercised sync on a CLI with more than a
handful of resources.
Force concurrency to 1 only when cliutil.IsVerifyEnv() returns true; real-world
sync keeps the parallel worker pool unchanged.
Touches sync.go.tmpl and graphql_sync.go.tmpl plus generator regression tests
that pin the import, the IsVerifyEnv() guard, and the after-default ordering.
Three sync.go golden fixtures refresh accordingly.
Closes #1205
* fix(cli): correct misleading ordering rationale in sync verify-env test helper
Greptile flagged the comment claim that placing the verify-env block before
the default-resolution block "would reset the pin on every call." That's
wrong — `1 < 1` is false, so the default block is a no-op once the verify
block sets concurrency to 1, regardless of ordering.
Reframe the assertion as a defensive guard against template churn rather
than a correctness invariant, and remove the false rationale from the
helper's docstring and assertion message.
---
internal/generator/generator_test.go | 100 +++++++++++++++++++++
internal/generator/templates/graphql_sync.go.tmpl | 8 ++
internal/generator/templates/sync.go.tmpl | 8 ++
.../printing-press-golden/internal/cli/sync.go | 8 ++
.../sync-walker-golden/internal/cli/sync.go | 8 ++
.../tier-routing-golden/internal/cli/sync.go | 8 ++
6 files changed, 140 insertions(+)
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index 0807cda8..d41f0486 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -7521,6 +7521,106 @@ func TestGeneratedSyncMaxPagesAndStickyCursor(t *testing.T) {
runGoCommand(t, outputDir, "build", "./...")
}
+// assertVerifyEnvConcurrencyPin pins the verify-mode worker-pool override
+// in generated sync.go: cliutil import present, IsVerifyEnv() guard pins
+// concurrency to 1, and the guard sits after the default-resolution block.
+// Ordering is not load-bearing for correctness today (the default block's
+// `if concurrency < 1 { concurrency = 4 }` is a no-op when concurrency is
+// already 1), but pinning it as a defensive guard prevents future template
+// churn from drifting the placement and obscuring the worker-pool comment
+// block this override controls.
+func assertVerifyEnvConcurrencyPin(t *testing.T, syncContent, cliutilImportPath, label string) {
+ t.Helper()
+ assert.Contains(t, syncContent, `"`+cliutilImportPath+`"`,
+ label+": sync.go must import internal/cliutil to call IsVerifyEnv")
+ assert.Contains(t, syncContent, "if cliutil.IsVerifyEnv() {",
+ label+": sync.go must consult cliutil.IsVerifyEnv() before starting the worker pool")
+ assert.Regexp(t,
+ `if cliutil\.IsVerifyEnv\(\) \{\s*concurrency = 1\s*\}`,
+ syncContent,
+ label+": verify-env block must pin concurrency to 1")
+
+ defaultIdx := strings.Index(syncContent, "concurrency = 4")
+ verifyIdx := strings.Index(syncContent, "cliutil.IsVerifyEnv()")
+ require.NotEqual(t, -1, defaultIdx, label+": default-resolution block must be present")
+ require.NotEqual(t, -1, verifyIdx, label+": verify-env block must be present")
+ assert.Less(t, defaultIdx, verifyIdx,
+ label+": verify-env override must sit after the default-resolution block (as shipped)")
+}
+
+// TestGeneratedSyncForcesSingleWorkerUnderVerifyEnv pins the verify-mode
+// override in the REST sync template. Without it, the worker pool races on
+// SQLite writes once network latency disappears, tripping SQLITE_BUSY
+// despite _busy_timeout — which fails validate-narrative --full-examples
+// for any quickstart or recipe that exercises sync.
+func TestGeneratedSyncForcesSingleWorkerUnderVerifyEnv(t *testing.T) {
+ t.Parallel()
+
+ apiSpec := &spec.APISpec{
+ Name: "verifysync",
+ Version: "0.1.0",
+ BaseURL: "https://api.example.com",
+ Auth: spec.AuthConfig{
+ Type: "api_key",
+ Header: "Authorization",
+ Format: "Bearer {token}",
+ EnvVars: []string{"VERIFYSYNC_API_KEY"},
+ },
+ Config: spec.ConfigSpec{
+ Format: "toml",
+ Path: "~/.config/verifysync-pp-cli/config.toml",
+ },
+ Resources: map[string]spec.Resource{
+ "channels": {
+ Description: "Manage channels",
+ Endpoints: map[string]spec.Endpoint{
+ "list": {
+ Method: "GET",
+ Path: "/channels",
+ Description: "List channels",
+ Response: spec.ResponseDef{Type: "array"},
+ },
+ },
+ },
+ },
+ }
+
+ outputDir := filepath.Join(t.TempDir(), naming.CLI(apiSpec.Name))
+ gen := New(apiSpec, outputDir)
+ require.NoError(t, gen.Generate())
+
+ syncGo, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "sync.go"))
+ require.NoError(t, err)
+
+ assertVerifyEnvConcurrencyPin(t, string(syncGo), naming.CLI(apiSpec.Name)+"/internal/cliutil", "REST sync.go")
+
+ runGoCommand(t, outputDir, "mod", "tidy")
+ runGoCommand(t, outputDir, "build", "./...")
+}
+
+// TestGeneratedGraphQLSyncForcesSingleWorkerUnderVerifyEnv pins the same
+// override in the GraphQL sync template. graphql_sync.go.tmpl drives
+// sync.go when isGraphQLSpec selects it; the race-on-SQLite problem under
+// PRINTING_PRESS_VERIFY=1 applies the same way.
+func TestGeneratedGraphQLSyncForcesSingleWorkerUnderVerifyEnv(t *testing.T) {
+ t.Parallel()
+
+ gqlSpec, err := graphql.ParseSDL(filepath.Join("..", "..", "testdata", "graphql", "test.graphql"))
+ require.NoError(t, err)
+
+ outputDir := filepath.Join(t.TempDir(), naming.CLI(gqlSpec.Name))
+ gen := New(gqlSpec, outputDir)
+ require.NoError(t, gen.Generate())
+
+ syncGo, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "sync.go"))
+ require.NoError(t, err)
+
+ assertVerifyEnvConcurrencyPin(t, string(syncGo), naming.CLI(gqlSpec.Name)+"/internal/cliutil", "GraphQL sync.go")
+
+ runGoCommand(t, outputDir, "mod", "tidy")
+ runGoCommand(t, outputDir, "build", "./...")
+}
+
// TestGeneratedSyncGatesSinceParamPerResource pins the fix for issue #900:
// generators must only inject the incremental-cursor query parameter on
// resources whose list endpoint actually declares it. Resources that don't
diff --git a/internal/generator/templates/graphql_sync.go.tmpl b/internal/generator/templates/graphql_sync.go.tmpl
index 0525ec57..98248e15 100644
--- a/internal/generator/templates/graphql_sync.go.tmpl
+++ b/internal/generator/templates/graphql_sync.go.tmpl
@@ -14,6 +14,7 @@ import (
"time"
"{{modulePath}}/internal/client"
+ "{{modulePath}}/internal/cliutil"
"{{modulePath}}/internal/store"
"github.com/spf13/cobra"
)
@@ -126,6 +127,13 @@ Exit codes & warnings:
if concurrency < 1 {
concurrency = 4
}
+ // Under PRINTING_PRESS_VERIFY=1 (mock/dry-run), all goroutines
+ // reach SQLite without the natural serialization that network
+ // latency provides in real syncs, so the worker pool races on
+ // the writer and trips SQLITE_BUSY despite _busy_timeout.
+ if cliutil.IsVerifyEnv() {
+ concurrency = 1
+ }
started := time.Now()
work := make(chan string, len(resources))
diff --git a/internal/generator/templates/sync.go.tmpl b/internal/generator/templates/sync.go.tmpl
index 2faf3001..8848f4b9 100644
--- a/internal/generator/templates/sync.go.tmpl
+++ b/internal/generator/templates/sync.go.tmpl
@@ -21,6 +21,7 @@ import (
{{- if .HasTierRouting}}
"{{modulePath}}/internal/client"
{{- end}}
+ "{{modulePath}}/internal/cliutil"
"{{modulePath}}/internal/store"
"github.com/spf13/cobra"
)
@@ -220,6 +221,13 @@ Resource scoping:
if concurrency < 1 {
concurrency = 4
}
+ // Under PRINTING_PRESS_VERIFY=1 (mock/dry-run), all goroutines
+ // reach SQLite without the natural serialization that network
+ // latency provides in real syncs, so the worker pool races on
+ // the writer and trips SQLITE_BUSY despite _busy_timeout.
+ if cliutil.IsVerifyEnv() {
+ concurrency = 1
+ }
started := time.Now()
work := make(chan string, len(resources))
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/sync.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/sync.go
index ed2ab002..ce6ad48a 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/sync.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/sync.go
@@ -9,6 +9,7 @@ import (
"github.com/spf13/cobra"
"net/url"
"os"
+ "printing-press-golden-pp-cli/internal/cliutil"
"printing-press-golden-pp-cli/internal/store"
"regexp"
"strconv"
@@ -185,6 +186,13 @@ Resource scoping:
if concurrency < 1 {
concurrency = 4
}
+ // Under PRINTING_PRESS_VERIFY=1 (mock/dry-run), all goroutines
+ // reach SQLite without the natural serialization that network
+ // latency provides in real syncs, so the worker pool races on
+ // the writer and trips SQLITE_BUSY despite _busy_timeout.
+ if cliutil.IsVerifyEnv() {
+ concurrency = 1
+ }
started := time.Now()
work := make(chan string, len(resources))
diff --git a/testdata/golden/expected/generate-sync-walker-api/sync-walker-golden/internal/cli/sync.go b/testdata/golden/expected/generate-sync-walker-api/sync-walker-golden/internal/cli/sync.go
index b2fff506..0d874121 100644
--- a/testdata/golden/expected/generate-sync-walker-api/sync-walker-golden/internal/cli/sync.go
+++ b/testdata/golden/expected/generate-sync-walker-api/sync-walker-golden/internal/cli/sync.go
@@ -13,6 +13,7 @@ import (
"strconv"
"strings"
"sync"
+ "sync-walker-golden-pp-cli/internal/cliutil"
"sync-walker-golden-pp-cli/internal/store"
"sync/atomic"
"time"
@@ -185,6 +186,13 @@ Resource scoping:
if concurrency < 1 {
concurrency = 4
}
+ // Under PRINTING_PRESS_VERIFY=1 (mock/dry-run), all goroutines
+ // reach SQLite without the natural serialization that network
+ // latency provides in real syncs, so the worker pool races on
+ // the writer and trips SQLITE_BUSY despite _busy_timeout.
+ if cliutil.IsVerifyEnv() {
+ concurrency = 1
+ }
started := time.Now()
work := make(chan string, len(resources))
diff --git a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/sync.go b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/sync.go
index 00ac7fa5..0c907a09 100644
--- a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/sync.go
+++ b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/sync.go
@@ -15,6 +15,7 @@ import (
"sync"
"sync/atomic"
"tier-routing-golden-pp-cli/internal/client"
+ "tier-routing-golden-pp-cli/internal/cliutil"
"tier-routing-golden-pp-cli/internal/store"
"time"
)
@@ -183,6 +184,13 @@ Resource scoping:
if concurrency < 1 {
concurrency = 4
}
+ // Under PRINTING_PRESS_VERIFY=1 (mock/dry-run), all goroutines
+ // reach SQLite without the natural serialization that network
+ // latency provides in real syncs, so the worker pool races on
+ // the writer and trips SQLITE_BUSY despite _busy_timeout.
+ if cliutil.IsVerifyEnv() {
+ concurrency = 1
+ }
started := time.Now()
work := make(chan string, len(resources))
← dbfc6118 fix(cli): propagate PRINTING_PRESS_VERIFY=1 to browser-sessi
·
back to Cli Printing Press
·
fix(cli): strip root-level binaries from staged publish tree 096411ce →