Skip to content
Merged
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
14 changes: 14 additions & 0 deletions docs/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,17 @@ and a dev tag on an off-main commit containing this workflow: both runs must
succeed, with seven assets each, and only the dev release marked prerelease
(and never Latest). Existing historical tags still contain their old workflow;
rerunning those runs does not exercise this change.

## Native shared-server qualification

Before releasing a change to native shared servers, run the real Nix lifecycle
suite, including `TestRunServerRefusesTwoServicesClaimingOneDatabase` with distinct
owner passwords and `TestRunServerSerializesInitialDatabaseClaims`. A refused
claim must leave the accepted service's credentials intact. Provisioning reuses
the run-server lock and checks claims before mutating roles. `TestMain` isolates
these servers from the operator's run-server directory.

Detached postmaster shutdown re-establishes the recorded boot/start identity and
uses Core's `runners/base.SignalProcess` for kernel-bound delivery. PostgreSQL
owns the shutdown signals and wait policy; it never signals a bare PID after
inspection. A stale birth is refused without signaling its replacement.
8 changes: 5 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
module github.com/codefly-dev/service-postgres

go 1.27.2
go 1.27.3

require (
github.com/codefly-dev/core v0.19.4
github.com/codefly-dev/core v0.19.8-0.20261027211051-bc4e15cab5b0
github.com/codefly-dev/gortk v0.2.0
github.com/codefly-dev/service-postgres/libs/go v0.0.0-20260915032422-272301033aed
github.com/gofrs/flock v0.13.1
Expand All @@ -27,7 +27,7 @@ require (
github.com/AzureAD/microsoft-authentication-library-for-go v1.8.0 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/semver/v3 v3.5.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 // indirect
github.com/VividCortex/ewma v1.2.0 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/blang/semver v3.5.1+incompatible // indirect
Expand All @@ -37,6 +37,7 @@ require (
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.2.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker v28.5.2+incompatible // indirect
Expand Down Expand Up @@ -106,6 +107,7 @@ require (
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.72.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 // indirect
go.opentelemetry.io/otel v1.47.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 // indirect
go.opentelemetry.io/otel/log v1.47.0 // indirect
go.opentelemetry.io/otel/metric v1.47.0 // indirect
go.opentelemetry.io/otel/trace v1.47.0 // indirect
Expand Down
32 changes: 16 additions & 16 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3Q
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 h1:0kQAzHq8vLs7Pptv+7TxjdETLf/nIqJpIB4oC6Ba4vY=
github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29/go.mod h1:ZWa7ssZJT30CCDGJ7fk/2SBTq9BIQrrVjrcss0UW2s0=
github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM=
github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow=
Expand All @@ -50,8 +50,8 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJ
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/codefly-dev/core v0.19.4 h1:vUvaFrWkB8B3ksQY/qlgBTVj3jMNhdEpJca8aofaals=
github.com/codefly-dev/core v0.19.4/go.mod h1:0uX/7lTBVaGV0RI2Mb3mpEr/PPdx/jZqXHym/U8FVDI=
github.com/codefly-dev/core v0.19.8-0.20261027211051-bc4e15cab5b0 h1:r5xxRusbZ54Wpk2T6aqqqNWvHmVVJ8HMJNdofE7mqrc=
github.com/codefly-dev/core v0.19.8-0.20261027211051-bc4e15cab5b0/go.mod h1:NB7x1UZ01rYTfEZEl4vUFD1A+fgOXhsWbgA3M8x4ThA=
github.com/codefly-dev/gortk v0.2.0 h1:7bOlS5valYz2zil+fZctQNcPCYBcPj86abcw9N8h1hQ=
github.com/codefly-dev/gortk v0.2.0/go.mod h1:dDWUMFgAP063OCGhTEpV7FFUj9+zTcxxO/nV80VKEwg=
github.com/codefly-dev/service-postgres/libs/go v0.0.0-20260915032422-272301033aed h1:P4H/FvViECu25UB7OEyCgyyDVsj6/Ql5rRPKCtcUklA=
Expand All @@ -60,8 +60,8 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/log v0.2.0 h1:BewD/umNgVnoczglOpX8eRMyEy5t5iPlu5AIpnWDONc=
github.com/containerd/log v0.2.0/go.mod h1:/M7L7CXKcPTfNC74XzaK+5H5KbO5+4lJVpuVI6vRLoM=
github.com/cyphar/filepath-securejoin v0.7.0 h1:s0Y3ITPy6sQn5xt54DuYvTF8hu134ooYLUb58DX/HjE=
github.com/cyphar/filepath-securejoin v0.7.0/go.mod h1:ymLGms/u3BYaviIiuKFnUx8EkQEZeK6cInNoAPJA3o4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down Expand Up @@ -215,14 +215,14 @@ github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8=
github.com/moby/sys/sequential v0.7.0/go.mod h1:NfSTAp6V3fw4tmkD62PEcOKeZKquXT8VKCkf7aVR79o=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0=
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4=
github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=
github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/oklog/ulid/v2 v2.1.2 h1:IEclFb9JNvzYA6MW2SCxbLzcHTVsfqm3PrqGQJH5zec=
github.com/oklog/ulid/v2 v2.1.2/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
Expand Down Expand Up @@ -253,8 +253,8 @@ github.com/shoenig/go-m1cpu v0.2.2 h1:4nc55oVv7nygGnfI9bhLCLzUEs4794y0Bkqx4q2zy7
github.com/shoenig/go-m1cpu v0.2.2/go.mod h1:KkDOw6m3ZJQAPHbrzkZki4hnx+pDRR1Lo+ldA56wD5w=
github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk=
github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg=
github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
Expand Down Expand Up @@ -286,8 +286,8 @@ go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM=
go.opentelemetry.io/otel v1.47.0/go.mod h1:8wS9O2qfXrYrzp6hIF/HOYJJf/wIhFPhR2xLuP+iXQU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 h1:julhjPeUH/q/7hinbSdDdqt5h7Zw9YWmRlWRhI0jd54=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0/go.mod h1:Ao2mz688LH/tFf0yMAenidq6k2YNSx6SIY2q6jDACck=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0 h1:QBajQ2SrwQijzHyZbQlPsuIzpl/ll8DY6wPWsajeGcI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0/go.mod h1:08ZQLjrPLQ6R4kAXvuOvODEer5Yh4CoFvll5qB2BCI8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 h1:aXZXsZ012wOgVkqaiQv+Z/d8V0xKjmg70F6m4CE94lc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0/go.mod h1:ziu1gUIJhAaxM3EV1e3Ralk5AAyL1UOYz3lJJ4VSI38=
go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI=
go.opentelemetry.io/otel/log v1.47.0/go.mod h1:9byitSQ5pLC6PpqwGXjqdMKya6ZTswHRZh2vvXT33nw=
go.opentelemetry.io/otel/metric v1.47.0 h1:4PptaldXx3Eat1XjMZ68pPJEs5wrhlemctZE9a3UdWY=
Expand Down Expand Up @@ -326,8 +326,8 @@ golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc h1:rZHRz0ogq4Pid1IKtA/ivHi8XONVF5tqUQXA2TtgpkE=
Expand Down
88 changes: 44 additions & 44 deletions nixpg.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ func acquireRunServer(ctx context.Context, key, service, logLevel string, out io
}

// lockRunServer serializes every start, attach, release and collection of one
// run server across the run's agent processes.
// run server across the run's agent processes, including database provisioning.
func lockRunServer(ctx context.Context, root string) (func(), error) {
lock := flock.New(filepath.Join(root, "lock"))
locked, err := lock.TryLockContext(ctx, 100*time.Millisecond)
Expand Down Expand Up @@ -673,12 +673,44 @@ func (n *nixPostgres) ping(ctx context.Context) error {
// another service's database, copy it, or alter another service's roles. A
// database another service of the run already claimed is refused by name.
func (n *nixPostgres) provisionDatabase(ctx context.Context, service, database, owner, password string) error {
unlock, err := lockRunServer(ctx, n.root)
if err != nil {
return err
}
defer unlock()

db, err := sql.Open("postgres", n.adminDSN("postgres"))
if err != nil {
return err
}
defer db.Close()

// The database records the service it belongs to, and that record is a
// comment any role with CREATEDB could have written on a database of its
// own. Its current owner is the half that cannot be forged: a service
// creating a database owns it, and it cannot hand ownership to an owner
// role it is not a member of — which the membership check above is what
// keeps true. A database owned by this service's own owner with no comment
// yet is the one legitimate gap, between CREATE DATABASE and COMMENT, and
// the comment is simply written.
marker := databaseClaim(service)
var claim sql.NullString
var currentOwner string
err = db.QueryRowContext(ctx,
`SELECT shobj_description(oid, 'pg_database'), pg_get_userbyid(datdba) FROM pg_database WHERE datname = $1`,
database).Scan(&claim, &currentOwner)
createDatabase := errors.Is(err, sql.ErrNoRows)
switch {
case err != nil && !createDatabase:
return fmt.Errorf("look up database %q: %w", database, err)
case !createDatabase && currentOwner != owner:
return fmt.Errorf("database %q on this run's postgres server is owned by %q, not by %s's migration owner: give each service of a run its own database-name",
database, currentOwner, service)
case !createDatabase && claim.Valid && claim.String != marker:
return fmt.Errorf("database %q on this run's postgres server belongs to %s, not %s: give each service of a run its own database-name",
database, strings.TrimPrefix(claim.String, databaseClaimPrefix), service)
}

// A migration owner may create roles, because its migrations create the
// application's own NOLOGIN roles. On PostgreSQL 16 and later a role
// created that way is granted back to its creator WITH ADMIN OPTION, so a
Expand Down Expand Up @@ -710,38 +742,13 @@ func (n *nixPostgres) provisionDatabase(ctx context.Context, service, database,
return fmt.Errorf("configure the migration owner: %w", err)
}

// The database records the service it belongs to, and that record is a
// comment any role with CREATEDB could have written on a database of its
// own. Its current owner is the half that cannot be forged: a service
// creating a database owns it, and it cannot hand ownership to an owner
// role it is not a member of — which the membership check above is what
// keeps true. A database owned by this service's own owner with no comment
// yet is the one legitimate gap, between CREATE DATABASE and COMMENT, and
// the comment is simply written.
marker := databaseClaim(service)
var claim sql.NullString
var currentOwner string
err = db.QueryRowContext(ctx,
`SELECT shobj_description(oid, 'pg_database'), pg_get_userbyid(datdba) FROM pg_database WHERE datname = $1`,
database).Scan(&claim, &currentOwner)
quotedDatabase := pq.QuoteIdentifier(database)
switch {
case errors.Is(err, sql.ErrNoRows):
if createDatabase {
if _, err := db.ExecContext(ctx, `CREATE DATABASE `+quotedDatabase+` OWNER `+quotedOwner); err != nil {
return fmt.Errorf("create database %q: %w", database, err)
}
if _, err := db.ExecContext(ctx, `COMMENT ON DATABASE `+quotedDatabase+` IS `+pq.QuoteLiteral(marker)); err != nil {
return fmt.Errorf("record database %q's service: %w", database, err)
}
case err != nil:
return fmt.Errorf("look up database %q: %w", database, err)
case currentOwner != owner:
return fmt.Errorf("database %q on this run's postgres server is owned by %q, not by %s's migration owner: give each service of a run its own database-name",
database, currentOwner, service)
case claim.Valid && claim.String != marker:
return fmt.Errorf("database %q on this run's postgres server belongs to %s, not %s: give each service of a run its own database-name",
database, strings.TrimPrefix(claim.String, databaseClaimPrefix), service)
case !claim.Valid:
}
if !claim.Valid {
if _, err := db.ExecContext(ctx, `COMMENT ON DATABASE `+quotedDatabase+` IS `+pq.QuoteLiteral(marker)); err != nil {
return fmt.Errorf("record database %q's service: %w", database, err)
}
Expand Down Expand Up @@ -942,25 +949,18 @@ func stopServer(root, dataDir string) (bool, error) {
return true, nil
}

// signalPostmaster delivers a signal to a recorded postmaster, and only while
// that exact birth is still the process behind the number.
//
// The confirmation and the delivery are two system calls, so a process that
// exits between them cannot be signalled by this call at all (the signal is
// then rejected, as the PID no longer resolves) unless the number is reused in
// that same window. Narrowing it to those microseconds is the most a caller
// can do for a process that is not its child; what this removes is the real
// exposure, which is a file on disk naming a PID whose process died minutes,
// hours or a reboot ago.
// signalPostmaster re-establishes the recorded birth, then delegates delivery
// to Core's kernel-bound process handle. A PID reused after inspection cannot
// receive the signal; PostgreSQL shutdown policy remains here.
func signalPostmaster(postmaster processMark, signal syscall.Signal) error {
if !postmaster.confirmed() {
return fmt.Errorf("postgres server %d is no longer the process this run started", postmaster.PID)
}
process, err := os.FindProcess(postmaster.PID)
identity, err := runners.InspectProcess(postmaster.PID)
if err != nil {
return err
}
return process.Signal(signal)
if identity.StartID != postmaster.StartID || identity.BootID != postmaster.BootID {
return fmt.Errorf("postgres server %d is no longer the process this run started: %w", postmaster.PID, runners.ErrProcessNotFound)
}
return runners.SignalProcess(context.Background(), identity, signal)
}

// forgetPostmasterRecord drops a record no server answers to any more, so a
Expand Down
40 changes: 40 additions & 0 deletions nixpg_shared_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"context"
"database/sql"
"fmt"
"net/url"
"os"
"path/filepath"
Expand Down Expand Up @@ -127,6 +128,11 @@ func TestRunServerRefusesTwoServicesClaimingOneDatabase(t *testing.T) {
workspace := t.TempDir()
first := newPostgresFixtureAt(t, ctx, workspace, "shared", "first", resources.NewRuntimeContextNix())
second := newPostgresFixtureAt(t, ctx, workspace, "shared", "second", resources.NewRuntimeContextNix())
for _, value := range second.configuration.Infos[0].ConfigurationValues {
if value.Key == "POSTGRES_PASSWORD" {
value.Value = "second-owner-password"
}
}

run, _ := first.start(t, ctx)

Expand Down Expand Up @@ -253,3 +259,37 @@ func TestRunServerRefusesAnOwnerRoleAnotherServiceCreated(t *testing.T) {
require.Contains(t, init.GetStatus().GetMessage(), "was not created for victim/store")
require.Nil(t, refused.nixRuntime, "a refused service holds no server")
}

// Competing initial claims must choose one service before either can alter the
// shared owner role. The losing service's password must never become usable.
func TestRunServerSerializesInitialDatabaseClaims(t *testing.T) {
requireNix(t)
ctx := t.Context()
fixture := newPostgresFixtureAt(t, ctx, t.TempDir(), "seed", "store", resources.NewRuntimeContextNix())
run, _ := fixture.start(t, ctx)
const database = "contended"
owner := nativeOwnerRole(database)
start := make(chan struct{})
results := make(chan int, 2)
for i := range 2 {
go func() {
<-start
err := run.nixRuntime.provisionDatabase(ctx, fmt.Sprintf("claimant/%d", i), database, owner, fmt.Sprintf("password-%d", i))
if err != nil {
results <- -1
} else {
results <- i
}
}()
}
close(start)
first, second := <-results, <-results
require.NotEqual(t, first == -1, second == -1, "exactly one initial claim succeeds")
winner := first
if winner == -1 {
winner = second
}
connection := withDatabase(t, run.connection, database)
require.NoError(t, connects(ctx, withUser(t, connection, owner, fmt.Sprintf("password-%d", winner))))
require.Error(t, connects(ctx, withUser(t, connection, owner, fmt.Sprintf("password-%d", 1-winner))))
}
Loading
Loading