feat(11-01): add the conga job framework on River with transactional dispatch
- Pin River v0.47.0 (riverdatabasesql, rivertype) and tidy the example modules - lagoon.Migrate runs the summercms.conga set: River schema v7 and summer_jobs with the apparatus columns plus an internal river_job_id link - conga.Manager.Dispatch writes the summer_jobs row (status IN_PROGRESS) and the River job on the caller's *sql.Tx; a rollback leaves neither - conga.Job wraps typed job functions so plugins never import River - conga.StartWorker runs one client on riverdatabasesql.NewWithPgxListener with a single-connection LISTEN pool; the final failed attempt sets ERROR - TestListenPickupLatency: 30s poll, pickup under 1s; poll-only control 2s miss
This commit is contained in:
178
modules/conga/postgres_test.go
Normal file
178
modules/conga/postgres_test.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package conga
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"git.golem15.com/golem15/summercms/modules/compass"
|
||||
"git.golem15.com/golem15/summercms/modules/lagoon"
|
||||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
"github.com/testcontainers/testcontainers-go/modules/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var (
|
||||
congaPG *postgres.PostgresContainer
|
||||
congaSQL *sql.DB
|
||||
congaDSN string
|
||||
congaPGErr error
|
||||
dbSeq atomic.Int64
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
code := 1
|
||||
if !testShort() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
congaPGErr = startCongaPostgres(ctx)
|
||||
cancel()
|
||||
if congaPGErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "conga: testcontainers postgres: %v\n", congaPGErr)
|
||||
stopCongaPostgres()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
code = m.Run()
|
||||
stopCongaPostgres()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func testShort() bool {
|
||||
for _, a := range os.Args {
|
||||
if a == "-test.short" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func startCongaPostgres(ctx context.Context) error {
|
||||
ctr, err := postgres.Run(ctx,
|
||||
"postgres:16-alpine",
|
||||
postgres.WithDatabase("conga"),
|
||||
postgres.WithUsername("conga"),
|
||||
postgres.WithPassword("conga"),
|
||||
postgres.BasicWaitStrategies(),
|
||||
testcontainers.WithEnv(map[string]string{
|
||||
"POSTGRES_INITDB_ARGS": "--locale-provider=icu --icu-locale=pl-PL --encoding=UTF8",
|
||||
}),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
congaPG = ctr
|
||||
dsn, err := ctr.ConnectionString(ctx, "sslmode=disable")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db, err := sql.Open("pgx", dsn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
_ = db.Close()
|
||||
return err
|
||||
}
|
||||
congaSQL = db
|
||||
congaDSN = dsn
|
||||
return nil
|
||||
}
|
||||
|
||||
func stopCongaPostgres() {
|
||||
if congaSQL != nil {
|
||||
_ = congaSQL.Close()
|
||||
}
|
||||
if congaPG != nil {
|
||||
_ = testcontainers.TerminateContainer(congaPG)
|
||||
}
|
||||
}
|
||||
|
||||
func adminDB(t *testing.T) *sql.DB {
|
||||
t.Helper()
|
||||
if testing.Short() {
|
||||
t.Skip("requires testcontainers postgres")
|
||||
}
|
||||
if congaPGErr != nil {
|
||||
t.Fatalf("postgres unavailable: %v", congaPGErr)
|
||||
}
|
||||
if congaSQL == nil {
|
||||
t.Fatal("postgres unavailable: container was not started")
|
||||
}
|
||||
return congaSQL
|
||||
}
|
||||
|
||||
// migratedDB returns a dedicated ICU pl-PL database migrated with
|
||||
// lagoon.Migrate (River v7 and summer_jobs) plus its DSN.
|
||||
func migratedDB(t *testing.T) (*sql.DB, string) {
|
||||
t.Helper()
|
||||
admin := adminDB(t)
|
||||
ctx := t.Context()
|
||||
name := fmt.Sprintf("conga_%d", dbSeq.Add(1))
|
||||
if _, err := admin.ExecContext(ctx, `CREATE DATABASE `+name+` TEMPLATE template0 ENCODING 'UTF8' LOCALE_PROVIDER icu ICU_LOCALE 'pl-PL'`); err != nil && !strings.Contains(err.Error(), "already exists") {
|
||||
t.Fatalf("create %s: %v", name, err)
|
||||
}
|
||||
dsn, err := dsnWithDB(congaDSN, name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db, err := sql.Open("pgx", dsn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = db.Close()
|
||||
_, _ = admin.ExecContext(context.Background(), `DROP DATABASE IF EXISTS `+name+` WITH (FORCE)`)
|
||||
})
|
||||
gdb, err := lagoon.Use(ctx, db)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := lagoon.Migrate(gdb, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return db, dsn
|
||||
}
|
||||
|
||||
func dsnWithDB(dsn, name string) (string, error) {
|
||||
u, err := url.Parse(dsn)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
u.Path = "/" + name
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
// testApp returns an app whose config names dsn and which has published db
|
||||
// and a GORM handle on it. Extra config keys are set as overrides.
|
||||
func testApp(t *testing.T, db *sql.DB, dsn string, extra map[string]any) (*backpack.App, *gorm.DB) {
|
||||
t.Helper()
|
||||
cfg, err := compass.Open(compass.Options{Dir: t.TempDir(), Env: "testing", Environ: []string{}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := cfg.Set("database.dsn", dsn); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for k, v := range extra {
|
||||
if err := cfg.Set(k, v); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
app := backpack.New(cfg)
|
||||
gdb, err := lagoon.Use(t.Context(), db)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := lagoon.Publish(app, db, gdb); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return app, gdb
|
||||
}
|
||||
Reference in New Issue
Block a user