Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions apps/cli-go/internal/db/diff/pgdelta_template_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package diff

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// lastCodeLine returns the final non-blank, non-comment line of a script.
func lastCodeLine(script string) string {
lines := strings.Split(script, "\n")
for i := len(lines) - 1; i >= 0; i-- {
line := strings.TrimSpace(lines[i])
if line == "" || strings.HasPrefix(line, "//") {
continue
}
return line
}
return ""
}

// Every pg-delta edge-runtime script must force the worker's event loop closed
// once its output has been written. The pg connection pool can leave keepalive
// handles registered even after close() resolves; if the worker never exits,
// the container never stops and the CLI — which streams the container logs with
// Follow:true — blocks forever following them, hanging declarative sync at 0%
// CPU (supabase/pg-toolbelt#312). The success path must terminate
// unconditionally rather than rely on the event loop draining on its own, so
// guard against the force-close being dropped from any template's success path.
func TestPgDeltaScriptsForceCloseOnSuccess(t *testing.T) {
scripts := map[string]string{
"pgdelta.ts": pgDeltaScript,
"pgdelta_declarative_export.ts": pgDeltaDeclarativeExportScript,
"pgdelta_catalog_export.ts": pgDeltaCatalogExportScript,
}
Comment on lines +33 to +37

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Include pgcache catalog export in the force-close fix

When pg-delta catalog caching is enabled, pgcache.TryCacheMigrationsCatalog still runs the separate pgDeltaCatalogExportTS script in apps/cli-go/internal/db/pgcache/cache.go through RunEdgeRuntimeScript; that script uses the same createManagedPool/extractCatalog/close() pattern but still ends after await close() with no final force-close throw. Because this new coverage only checks the three diff-package templates, the hang fixed here can still occur on the migrations-catalog cache path, such as db start/db push with pg-delta caching enabled. Please update that template and cover it as well.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed. pgDeltaCatalogExportTS in internal/db/pgcache/cache.go (the db start / db push migrations-catalog cache path) had the same createManagedPool/close() pattern with no success-path force-close. Added the force-close there plus a cache_template_test.go guard, matching the other four scripts. Pushed in 7908b63.


Generated by Claude Code

for name, script := range scripts {
t.Run(name, func(t *testing.T) {
require.NotEmpty(t, script)
// The terminating statement runs on the success path (the catch
// branch no longer re-throws), so the worker is torn down whether
// or not the body succeeded.
assert.Equal(t, `throw new Error("");`, lastCodeLine(script),
"success path must force the Edge Runtime worker to exit so the container stops")
})
}
}
6 changes: 6 additions & 0 deletions apps/cli-go/internal/db/diff/templates/pgdelta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,9 @@ try {
// Force close event loop
throw new Error("");
}
// Force close the event loop on the success path too. When SOURCE/TARGET are
// live database URLs the plan opens connections whose keepalive handles can keep
// the Edge Runtime worker alive after the diff has been written, so the container
// never exits and the CLI — which follows this container's logs — hangs
// indefinitely at 0% CPU (supabase/pg-toolbelt#312).
throw new Error("");
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,16 @@ try {
console.log(stringifyCatalogSnapshot(serializeCatalog(catalog)));
} catch (e) {
console.error(e);
// Force close event loop
throw new Error("");
} finally {
await close();
}
// Force close the event loop on the success path too. The connection pool can
// leave keepalive handles registered even after close() resolves, which keeps
// the Edge Runtime worker (and therefore the container) alive after the catalog
// has already been written to stdout. The CLI streams this container's logs with
// Follow:true, so a worker that never exits hangs the parent `__catalog`
// subprocess — and the declarative-sync command that spawned it — indefinitely
// at 0% CPU (supabase/pg-toolbelt#312).
throw new Error("");
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,9 @@ try {
// Force close event loop
throw new Error("");
}
// Force close the event loop on the success path too. When SOURCE/TARGET are
// live database URLs the plan opens connections whose keepalive handles can keep
// the Edge Runtime worker alive after the export has been written, so the
// container never exits and the CLI — which follows this container's logs —
// hangs indefinitely at 0% CPU (supabase/pg-toolbelt#312).
throw new Error("");
9 changes: 9 additions & 0 deletions apps/cli-go/internal/db/pgcache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,19 @@ try {
console.log(stringifyCatalogSnapshot(serializeCatalog(catalog)));
} catch (e) {
console.error(e);
// Force close event loop
throw new Error("");
} finally {
await close();
}
// Force close the event loop on the success path too. The connection pool can
// leave keepalive handles registered even after close() resolves, which keeps
// the Edge Runtime worker (and therefore the container) alive after the catalog
// has already been written to stdout. The CLI streams this container's logs with
// Follow:true, so a worker that never exits hangs the migrations-catalog cache
// path (db start / db push with pg-delta caching) indefinitely at 0% CPU
// (supabase/pg-toolbelt#312).
throw new Error("");
`
)

Expand Down
33 changes: 33 additions & 0 deletions apps/cli-go/internal/db/pgcache/cache_template_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package pgcache

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// The migrations-catalog cache script (db start / db push with pg-delta caching)
// opens a connection pool and must force the worker's event loop closed once it
// has written its snapshot. If a keepalive handle lingers after close() resolves
// the worker never exits, so the container never stops and the CLI — which
// follows the container logs with Follow:true — hangs indefinitely at 0% CPU
// (supabase/pg-toolbelt#312). Guard against the success-path force-close being
// dropped.
func TestPgDeltaCatalogExportScriptForceClosesOnSuccess(t *testing.T) {
require.NotEmpty(t, pgDeltaCatalogExportTS)

lines := strings.Split(pgDeltaCatalogExportTS, "\n")
last := ""
for i := len(lines) - 1; i >= 0; i-- {
line := strings.TrimSpace(lines[i])
if line == "" || strings.HasPrefix(line, "//") {
continue
}
last = line
break
}
assert.Equal(t, `throw new Error("");`, last,
"success path must force the Edge Runtime worker to exit so the container stops")
}
33 changes: 33 additions & 0 deletions apps/cli-go/internal/pgdelta/pgdelta_apply_template_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package pgdelta

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// The declarative-apply script connects to TARGET and must force the worker's
// event loop closed once it has written its result JSON. applyDeclarativeSchema
// can leave connection keepalive handles registered, and if the worker never
// exits the container never stops — the CLI, which follows the container logs
// with Follow:true, then hangs indefinitely at 0% CPU (supabase/pg-toolbelt#312).
// The success path must terminate unconditionally, so guard against the
// force-close being dropped.
func TestDeclarativeApplyScriptForceClosesOnSuccess(t *testing.T) {
require.NotEmpty(t, pgDeltaDeclarativeApplyScript)

lines := strings.Split(pgDeltaDeclarativeApplyScript, "\n")
last := ""
for i := len(lines) - 1; i >= 0; i-- {
line := strings.TrimSpace(lines[i])
if line == "" || strings.HasPrefix(line, "//") {
continue
}
last = line
break
}
assert.Equal(t, `throw new Error("");`, last,
"success path must force the Edge Runtime worker to exit so the container stops")
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,10 @@ try {
} catch (e) {
throw e instanceof Error ? e : new Error(String(e));
}
// Force close the event loop on the success path. applyDeclarativeSchema opens a
// connection to TARGET whose keepalive handles can keep the Edge Runtime worker
// alive after the result JSON has been written, so the container never exits and
// the CLI — which follows this container's logs — hangs indefinitely at 0% CPU
// (supabase/pg-toolbelt#312). The catch above re-throws the real error, so this
// only runs once a successful apply has been reported on stdout.
throw new Error("");
Loading
Loading