From b7a7f2a82f4d8423a8c1b458cae8ffc1d9f56326 Mon Sep 17 00:00:00 2001 From: Bel LaPointe <153096461+breel-render@users.noreply.github.com> Date: Thu, 24 Apr 2025 21:50:18 -0600 Subject: [PATCH] WIP --- go.mod | 15 ++++++++++ go.sum | 47 ++++++++++++++++++++++++++++++ src/cleanup/cleanup.go | 28 ++++++++++++++++++ src/cleanup/cleanup_test.go | 33 +++++++++++++++++++++ src/cmd/config.go | 15 ++++++++++ src/cmd/main.go | 5 ++++ src/db/ctx.go | 57 +++++++++++++++++++++++++++++++++++++ src/db/db.go | 29 +++++++++++++++++++ src/db/db_test.go | 49 +++++++++++++++++++++++++++++++ 9 files changed, 278 insertions(+) create mode 100644 go.sum create mode 100644 src/cleanup/cleanup.go create mode 100644 src/cleanup/cleanup_test.go create mode 100644 src/cmd/config.go create mode 100644 src/db/ctx.go create mode 100644 src/db/db.go create mode 100644 src/db/db_test.go diff --git a/go.mod b/go.mod index cc84423..8a06f73 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,18 @@ module show-rss go 1.23.3 + +require modernc.org/sqlite v1.37.0 + +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect + golang.org/x/sys v0.31.0 // indirect + modernc.org/libc v1.62.1 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.9.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e944b01 --- /dev/null +++ b/go.sum @@ -0,0 +1,47 @@ +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +modernc.org/cc/v4 v4.25.2 h1:T2oH7sZdGvTaie0BRNFbIYsabzCxUQg8nLqCdQ2i0ic= +modernc.org/cc/v4 v4.25.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.25.1 h1:TFSzPrAGmDsdnhT9X2UrcPMI3N/mJ9/X9ykKXwLhDsU= +modernc.org/ccgo/v4 v4.25.1/go.mod h1:njjuAYiPflywOOrm3B7kCB444ONP5pAVr8PIEoE0uDw= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/libc v1.62.1 h1:s0+fv5E3FymN8eJVmnk0llBe6rOxCu/DEU+XygRbS8s= +modernc.org/libc v1.62.1/go.mod h1:iXhATfJQLjG3NWy56a6WVU73lWOcdYVxsvwCgoPljuo= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g= +modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI= +modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/src/cleanup/cleanup.go b/src/cleanup/cleanup.go new file mode 100644 index 0000000..b07da8f --- /dev/null +++ b/src/cleanup/cleanup.go @@ -0,0 +1,28 @@ +package cleanup + +import "context" + +const ctxKey = "__cleanup" + +func Inject(ctx context.Context, foo func()) context.Context { + before := Extract(ctx) + after := func() { + foo() + before() + } + return context.WithValue(ctx, ctxKey, after) +} + +func Extract(ctx context.Context) func() { + v := ctx.Value(ctxKey) + if v == nil { + return func() {} + } + + v2, _ := v.(func()) + if v2 == nil { + return func() {} + } + + return v2 +} diff --git a/src/cleanup/cleanup_test.go b/src/cleanup/cleanup_test.go new file mode 100644 index 0000000..5e2def9 --- /dev/null +++ b/src/cleanup/cleanup_test.go @@ -0,0 +1,33 @@ +package cleanup_test + +import ( + "context" + "show-rss/src/cleanup" + "testing" +) + +func TestCleanup(t *testing.T) { + ctx := context.Background() + + called := make([]bool, 100) + for i := range called { + i := i + ctx = cleanup.Inject(ctx, func() { + t.Logf("cleaning %d", i) + if i < len(called)-1 && !called[i+1] { + t.Errorf("cleaning %d before %d", i, i+1) + } + called[i] = true + }) + } + + t.Logf("cleaning") + cleanup.Extract(ctx)() + t.Logf("cleaned") + + for i := range called { + if !called[i] { + t.Fatalf("missing called[%d]", i) + } + } +} diff --git a/src/cmd/config.go b/src/cmd/config.go new file mode 100644 index 0000000..8e78a05 --- /dev/null +++ b/src/cmd/config.go @@ -0,0 +1,15 @@ +package cmd + +import ( + "context" + "show-rss/src/db" +) + +func Config(ctx context.Context) (context.Context, error) { + ctx, err := db.Inject(ctx) + if err != nil { + return ctx, err + } + + return ctx, nil +} diff --git a/src/cmd/main.go b/src/cmd/main.go index e3e5ea0..3467860 100644 --- a/src/cmd/main.go +++ b/src/cmd/main.go @@ -14,6 +14,11 @@ func Main(ctx context.Context) error { ctx, can := context.WithCancel(ctx) defer can() + ctx, err := Config(ctx) + if err != nil { + return fmt.Errorf("failed to inject: %w", err) + } + foos := map[string]func(context.Context) error{ "server": server.Main, "cron": cron.Main, diff --git a/src/db/ctx.go b/src/db/ctx.go new file mode 100644 index 0000000..7a6ede9 --- /dev/null +++ b/src/db/ctx.go @@ -0,0 +1,57 @@ +package db + +import ( + "context" + "fmt" + "time" + + "database/sql" + + "show-rss/src/cleanup" + + _ "modernc.org/sqlite" +) + +const ctxKey = "__db" + +func Inject(ctx context.Context, conn string) (context.Context, error) { + connctx, can := context.WithTimeout(ctx, 15*time.Second) + defer can() + + db, err := sql.Open("sqlite", conn) + if err != nil { + return ctx, err + } + ctx = cleanup.Inject(ctx, func() { + db.Close() + }) + + if err := func() error { + c := time.NewTicker(100 * time.Millisecond) + defer c.Stop() + + var err error + for connctx.Err() == nil { + if err = db.PingContext(connctx); err == nil { + return nil + } + select { + case <-connctx.Done(): + case <-c.C: + } + } + return err + }(); err != nil { + return ctx, err + } + + return context.WithValue(ctx, ctxKey, db), ctx.Err() +} + +func extract(ctx context.Context) (*sql.DB, error) { + db := ctx.Value(ctxKey) + if db == nil { + return nil, fmt.Errorf("db not injected") + } + return db.(*sql.DB), nil +} diff --git a/src/db/db.go b/src/db/db.go new file mode 100644 index 0000000..6a9500c --- /dev/null +++ b/src/db/db.go @@ -0,0 +1,29 @@ +package db + +import ( + "context" + "database/sql" +) + +func QueryOne(ctx context.Context, q string, args ...any) error { + return with(ctx, func(db *sql.DB) error { + row := db.QueryRowContext(ctx, q, args...) + TODO generic and return value + return row.Err() + }) +} + +func Exec(ctx context.Context, q string, args ...any) error { + return with(ctx, func(db *sql.DB) error { + _, err := db.ExecContext(ctx, q, args...) + return err + }) +} + +func with(ctx context.Context, foo func(*sql.DB) error) error { + db, err := extract(ctx) + if err != nil { + return err + } + return foo(db) +} diff --git a/src/db/db_test.go b/src/db/db_test.go new file mode 100644 index 0000000..c70ee94 --- /dev/null +++ b/src/db/db_test.go @@ -0,0 +1,49 @@ +package db_test + +import ( + "context" + "path" + "show-rss/src/cleanup" + "show-rss/src/db" + "testing" +) + +func TestDB(t *testing.T) { + ctx := context.Background() + + ctx, err := db.Inject(ctx, path.Join(t.TempDir(), "db")) + if err != nil { + t.Fatal(err) + } + defer func() { + cleanup.Extract(ctx)() + }() + + if err := db.Exec(ctx, ` + CREATE TABLE IF NOT EXISTS test (k TEXT); + CREATE UNIQUE INDEX IF NOT EXISTS test_idx ON test (k); + INSERT INTO test (k) SELECT 'a'; + INSERT INTO test (k) SELECT 'b'; + `); err != nil { + t.Fatal(err) + } + + var result struct { + K string + } + if got, err := db.QueryOne[result](ctx, `SELECT k FROM test WHERE k='a'`); err != nil { + t.Errorf("failed query one: %w", err) + } else if got.K != "a" { + t.Errorf("bad query one: %+v", got) + } + + if gots, err := db.Query[result](ctx, `SELECT k FROM test`); err != nil { + t.Errorf("failed query: %w", err) + } else if len(gots) != 2 { + t.Errorf("expected 2 but got %d gots", len(gots)) + } else if gots[0].K != "a" { + t.Errorf("expected [0]='a' but got %q", gots[0].K) + } else if gots[1].K != "b" { + t.Errorf("expected [1]='b' but got %q", gots[1].K) + } +}