Compare commits
15 Commits
48c3474c15
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 439c481c60 | |||
| 0a58f85ca3 | |||
| 141c0e5100 | |||
| 9f3de791f9 | |||
| 8d3db70e01 | |||
| 831fed82f7 | |||
| 43887bb447 | |||
| 0b9b790986 | |||
| ae54bee23b | |||
| 84a8897bf0 | |||
| 2e339cff36 | |||
| 52c00adfe7 | |||
| 1be18b80d2 | |||
| faae75d5f4 | |||
| a9f78655c3 |
@@ -0,0 +1,35 @@
|
||||
name: cicd
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
#paths:
|
||||
#- '/**'
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
name: ci
|
||||
runs-on: dind
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: ci
|
||||
run: |
|
||||
#! /bin/bash
|
||||
|
||||
set -ex
|
||||
|
||||
repo=$(grep url .git/config | awk '{print $NF}' | sed 's#.*\.[a-z]*/##' | sed 's#.git$##')
|
||||
test -n "$repo"
|
||||
if [[ "$repo" == *"template" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
img=registry-app.inhome.blapointe.com:5001/"$repo":"${1:-$(date +%Y%m%d%H%M)}"
|
||||
was=$((docker inspect $img | jq -r .[0].Id | sed 's/^sha256://') || true)
|
||||
docker build -f ./Dockerfile -t $img .
|
||||
now=$(docker inspect $img | jq -r .[0].Id | sed 's/^sha256://')
|
||||
docker push $img
|
||||
if [ -n "$was" ] && [ "$was" != "null" ] && [ "$was" != "$now" ] && docker inspect "$was" &> /dev/null; then
|
||||
docker rmi "$was" || true
|
||||
docker rmi $(docker ps | grep ${img%:*} | grep '<none>' | awk '{print $3}') || true
|
||||
fi
|
||||
@@ -0,0 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"gitea.bel.blue/bel/with"
|
||||
"gitea.bel.blue/bel/with/cmd/example-dbos/work"
|
||||
"gitea.bel.blue/bel/with/dbos"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := with.Context(run); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func run(ctx context.Context) error {
|
||||
fs := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
|
||||
conn := fs.String("c", "", "postgresql://u:p@h:p/db")
|
||||
gdedupe := fs.String("k1", "global-deduplication-key", "prevent ever re-evaluating this execution")
|
||||
cdedupe := fs.String("k2", "concurrent-deduplication-key", "prevent re-evaluating this execution concurrently")
|
||||
if err := fs.Parse(os.Args[1:]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return dbos.QueueClient(ctx, *conn, "queue", func(c *dbos.Client) error {
|
||||
return dbos.Go(ctx, c, work.Workflow, 1, *gdedupe, *cdedupe)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package work
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/dbos-inc/dbos-transact-golang/dbos"
|
||||
)
|
||||
|
||||
func Workflow(ctx dbos.DBOSContext, arg any) (any, error) {
|
||||
log.Printf("WORKFLOW with %v!", arg)
|
||||
return nil, nil
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"gitea.bel.blue/bel/with"
|
||||
"gitea.bel.blue/bel/with/cmd/example-dbos/work"
|
||||
mydbos "gitea.bel.blue/bel/with/dbos"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := with.Context(run); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func run(ctx context.Context) error {
|
||||
fs := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
|
||||
conn := fs.String("c", "", "postgresql://u:p@h:p/db")
|
||||
if err := fs.Parse(os.Args[1:]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return mydbos.QueueWorker(ctx, *conn, "queue", func(w *mydbos.Worker) error {
|
||||
mydbos.Can(w, work.Workflow)
|
||||
return w.Listen(ctx)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package with
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Context(foo func(context.Context) error) error {
|
||||
ctx, can := signal.NotifyContext(context.Background(), syscall.SIGINT)
|
||||
defer can()
|
||||
|
||||
if err := foo(ctx); err != nil && ctx.Err() == nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Timeout(ctx context.Context, d time.Duration, foo func(context.Context) error) error {
|
||||
ctx, can := context.WithTimeout(ctx, d)
|
||||
defer can()
|
||||
return foo(ctx)
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package dbos
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dbos-inc/dbos-transact-golang/dbos"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
c dbos.Client
|
||||
q string
|
||||
}
|
||||
|
||||
func QueueClient(ctx context.Context, conn, q string, foo func(*Client) error) error {
|
||||
connctx, conncan := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer conncan()
|
||||
c, err := dbos.NewClient(connctx, dbos.ClientConfig{
|
||||
DatabaseURL: conn,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer c.Shutdown(5 * time.Second)
|
||||
return foo(&Client{c: c, q: q})
|
||||
}
|
||||
|
||||
func (c *Client) WithQueue(q string) *Client {
|
||||
c2 := *c
|
||||
c2.q = q
|
||||
return &c2
|
||||
}
|
||||
|
||||
func Go[P any, R any](ctx context.Context, c *Client, foo dbos.Workflow[P, R], input P, globalDedupe, concurrentDedupe string) error {
|
||||
log.Printf("[client] going to %s#%s#%s", fooToName(foo), globalDedupe, concurrentDedupe)
|
||||
|
||||
result := make(chan error)
|
||||
go func() {
|
||||
defer close(result)
|
||||
_, err := dbos.Enqueue[P, R](c.c, c.q, fooToName(foo), input, dbos.WithEnqueueDeduplicationID(concurrentDedupe), dbos.WithEnqueueApplicationVersion("latest"), dbos.WithEnqueueWorkflowID(globalDedupe))
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case result <- err:
|
||||
}
|
||||
}()
|
||||
select {
|
||||
case err := <-result:
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
}
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
// https://github.com/dbos-inc/dbos-transact-golang/blob/0d755bda26e3162c823f3e316aa0f88677b8b246/dbos/workflow.go#L577
|
||||
func fooToName[P any, R any](fn dbos.Workflow[P, R]) string {
|
||||
ptr := reflect.ValueOf(fn).Pointer()
|
||||
fqn := runtime.FuncForPC(ptr).Name()
|
||||
|
||||
// If this is a generic workflow, append the actual types to the FQN
|
||||
if strings.Contains(fqn, "[") {
|
||||
fqn = strings.Split(fqn, "[")[0]
|
||||
fqn = fmt.Sprintf("%s[%s,%s]",
|
||||
fqn,
|
||||
reflect.TypeFor[P]().String(),
|
||||
reflect.TypeFor[R]().String(),
|
||||
)
|
||||
}
|
||||
|
||||
return fqn
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package dbos
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dbos-inc/dbos-transact-golang/dbos"
|
||||
)
|
||||
|
||||
func TestDBOS(t *testing.T) {
|
||||
conn := os.Getenv("CONN_URL")
|
||||
if conn == "" {
|
||||
t.Skip("no $CONN_URL")
|
||||
}
|
||||
|
||||
routine := func() string {
|
||||
buf := make([]byte, 128_000)
|
||||
n := runtime.Stack(buf[:], false)
|
||||
buf = buf[:n]
|
||||
|
||||
myRoutine := strings.Fields(strings.TrimPrefix(string(buf), "goroutine"))[0]
|
||||
|
||||
re := regexp.MustCompile(`created by .*? goroutine [0-9]+`)
|
||||
chain := []string{myRoutine}
|
||||
for _, match := range re.FindAllString(string(buf), -1) {
|
||||
fields := strings.Fields(match)
|
||||
chain = append(chain, fields[len(fields)-1])
|
||||
}
|
||||
return strings.Join(chain, "-")
|
||||
}
|
||||
|
||||
ctx, can := context.WithTimeout(context.Background(), 19*time.Second)
|
||||
defer can()
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
defer wg.Wait()
|
||||
wg.Add(3)
|
||||
waitScheduled := &sync.Once{}
|
||||
|
||||
onDemandWorkflow := func(ctx dbos.DBOSContext, arg string) (string, error) {
|
||||
t.Logf("[%s] on demand...", routine())
|
||||
defer t.Logf("[%s] /on demand", routine())
|
||||
|
||||
dbos.Sleep(ctx, 1*time.Millisecond)
|
||||
return dbos.RunAsStep(ctx, func(ctx context.Context) (string, error) {
|
||||
t.Logf("[%s] on demand step...", routine())
|
||||
defer t.Logf("[%s] /on demand step", routine())
|
||||
|
||||
t.Logf("[%s] step on demand", routine())
|
||||
wg.Done()
|
||||
return "my step string", nil
|
||||
})
|
||||
}
|
||||
|
||||
scheduledWorkflow := func(dbos.DBOSContext, time.Time) (int, error) {
|
||||
t.Logf("[%s] scheduled...", routine())
|
||||
defer t.Logf("[%s] /scheduled", routine())
|
||||
|
||||
waitScheduled.Do(wg.Done)
|
||||
return -1, nil
|
||||
}
|
||||
|
||||
t.Run("worker", func(t *testing.T) {
|
||||
go func() {
|
||||
if err := QueueWorker(ctx, conn, "queue", func(w *Worker) error {
|
||||
t.Logf("[%s] dbosc...", routine())
|
||||
defer t.Logf("[%s] /dbosc", routine())
|
||||
|
||||
t.Logf("[%s] dbosc.can...", routine())
|
||||
Can(w, onDemandWorkflow)
|
||||
t.Logf("[%s] dbosc.every...", routine())
|
||||
Every(w, scheduledWorkflow, "* * * * * *")
|
||||
t.Logf("[%s] dbosc go do...", routine())
|
||||
go func() {
|
||||
t.Logf("[%s] dbos do...", routine())
|
||||
defer t.Logf("[%s] /dbos do", routine())
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
res, err := Do(ctx, w, onDemandWorkflow, "arg")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("onDemandWorkflow result: %q", res)
|
||||
}()
|
||||
|
||||
t.Logf("[%s] dbosc listen...", routine())
|
||||
defer t.Logf("[%s] /dbosc listen", routine())
|
||||
return w.Listen(ctx)
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
})
|
||||
|
||||
t.Run("client", func(t *testing.T) {
|
||||
if err := QueueClient(ctx, conn, "queue", func(w *Client) error {
|
||||
t.Logf("[%s] dbosc...", routine())
|
||||
defer t.Logf("[%s] /dbosc", routine())
|
||||
|
||||
t.Logf("[%s] dbos go...", routine())
|
||||
defer t.Logf("[%s] /dbos go", routine())
|
||||
|
||||
err := Go(ctx, w, onDemandWorkflow, "arg", time.Now().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("[%s] onDemandWorkflow enqueued result", routine())
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package dbos
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/dbos-inc/dbos-transact-golang/dbos"
|
||||
)
|
||||
|
||||
type Worker struct {
|
||||
dbos dbos.DBOSContext
|
||||
q string
|
||||
qs []dbos.WorkflowQueue
|
||||
}
|
||||
|
||||
func QueueWorker(ctx context.Context, conn, q string, foo func(*Worker) error) error {
|
||||
return NewWorker(ctx, conn, func(w *Worker) error {
|
||||
return foo(w.WithQueue(q))
|
||||
})
|
||||
}
|
||||
|
||||
func NewWorker(ctx context.Context, conn string, foo func(*Worker) error) error {
|
||||
type result struct {
|
||||
ctx dbos.DBOSContext
|
||||
err error
|
||||
}
|
||||
ch := make(chan result)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
dbosctx, err := dbos.NewDBOSContext(ctx, dbos.Config{
|
||||
AppName: "with",
|
||||
DatabaseURL: conn,
|
||||
AdminServer: os.Getenv("WITH_DBOS_ADMIN_SERVER") == "true",
|
||||
ApplicationVersion: "latest",
|
||||
})
|
||||
select {
|
||||
case ch <- result{ctx: dbosctx, err: err}:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}()
|
||||
select {
|
||||
case result := <-ch:
|
||||
if err := result.err; err != nil {
|
||||
return err
|
||||
}
|
||||
return foo(&Worker{dbos: result.ctx})
|
||||
case <-ctx.Done():
|
||||
}
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func (c *Worker) Listen(ctx context.Context) error {
|
||||
dbos := c.dbos
|
||||
//dbos.ListenQueues(c.dbos, c.qs...)
|
||||
log.Printf("[worker] listening to %+v", c.q)
|
||||
if err := dbos.Launch(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer dbos.Shutdown(5 * time.Second)
|
||||
<-ctx.Done()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Worker) WithQueue(q string) *Worker {
|
||||
c2 := *c
|
||||
c2.q = q
|
||||
c2.qs = slices.Clone(c2.qs)
|
||||
|
||||
dbosq := dbos.NewWorkflowQueue(c2.dbos, q)
|
||||
//dbos.WithWorkerConcurrency(5), // per worker
|
||||
//dbos.WithGlobalConcurrency(10), // per all workers
|
||||
//dbos.WithRateLimiter(&dbos.RateLimiter{Limit: 100, Period: time.Second}),
|
||||
//dbos.WithPriorityEnabled(),
|
||||
// dbos.WithPartitionQueue(), // WithQueuePartitionKey but has limit per-partiiton
|
||||
|
||||
c2.qs = append(c2.qs, dbosq)
|
||||
|
||||
return &c2
|
||||
}
|
||||
|
||||
func Can[P any, R any](c *Worker, foo dbos.Workflow[P, R]) {
|
||||
log.Printf("[worker] registering %s", fooToName(foo))
|
||||
dbos.RegisterWorkflow(c.dbos, foo, dbos.WithWorkflowName(fooToName(foo)))
|
||||
}
|
||||
|
||||
func Every[P any, R any](w *Worker, foo dbos.Workflow[P, R], cron string) {
|
||||
log.Printf("[worker] registering cron '%s' %s", cron, fooToName(foo))
|
||||
dbos.RegisterWorkflow(w.dbos, foo, dbos.WithSchedule(cron), dbos.WithWorkflowName(fooToName(foo)))
|
||||
}
|
||||
|
||||
func Do[P any, R any](ctx context.Context, w *Worker, foo dbos.Workflow[P, R], input P) (R, error) {
|
||||
dbosctx := dbos.From(w.dbos, ctx)
|
||||
log.Printf("[worker] doing %s", fooToName(foo))
|
||||
handle, err := dbos.RunWorkflow(dbosctx, foo, input) //, options...)
|
||||
var some R
|
||||
if err != nil {
|
||||
return some, err
|
||||
}
|
||||
return handle.GetResult()
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package with
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GoEvery(ctx context.Context, d time.Duration, foo func()) {
|
||||
every(ctx, d, foo, true)
|
||||
}
|
||||
|
||||
func Every(ctx context.Context, d time.Duration, foo func()) {
|
||||
every(ctx, d, foo, false)
|
||||
}
|
||||
|
||||
func every(ctx context.Context, d time.Duration, foo func(), async bool) {
|
||||
ticker := time.NewTicker(d)
|
||||
defer ticker.Stop()
|
||||
for ctx.Err() == nil {
|
||||
everyTry(ctx, foo, async)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func everyTry(ctx context.Context, foo func(), async bool) {
|
||||
if async {
|
||||
go foo()
|
||||
} else {
|
||||
foo()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
module gitea.bel.blue/bel/with
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/dbos-inc/dbos-transact-golang v0.15.0
|
||||
github.com/lib/pq v1.11.2
|
||||
modernc.org/sqlite v1.46.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.9.1 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
modernc.org/libc v1.67.6 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,89 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dbos-inc/dbos-transact-golang v0.15.0 h1:h4RBiUv5AED6vSmM/CPAPAC8uAfBw+USz8xgULbzrdk=
|
||||
github.com/dbos-inc/dbos-transact-golang v0.15.0/go.mod h1:Q9UxpOFyrSBYQywcFyy4nDHCFThQwwNuQvSi8S9LDZY=
|
||||
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/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 h1:D/V0gu4zQ3cL2WKeVNVM4r2gLxGGf6McLwgXzRTo2RQ=
|
||||
github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc=
|
||||
github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
|
||||
github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
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 v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
|
||||
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
|
||||
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
|
||||
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
|
||||
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
|
||||
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.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/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.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
|
||||
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
||||
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=
|
||||
@@ -0,0 +1,140 @@
|
||||
package with
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RoundTripper struct {
|
||||
http.RoundTripper
|
||||
kv SQLKV
|
||||
}
|
||||
|
||||
func CachedHTTP(ctx context.Context, foo func(*http.Client) error) error {
|
||||
return Sqlite(ctx, ":memory:", func(db *sql.DB) error {
|
||||
return KV(ctx, db, func(kv SQLKV) error {
|
||||
return foo(&http.Client{
|
||||
Timeout: time.Minute,
|
||||
Transport: RoundTripper{
|
||||
RoundTripper: &http.Transport{
|
||||
DisableKeepAlives: true,
|
||||
},
|
||||
kv: kv,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (c RoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
req := newCacheableRequest(r)
|
||||
if v, err := c.kv.Get(r.Context(), req.cacheK()); err != nil {
|
||||
} else if resp := parseCacheableResponse(v); resp != nil {
|
||||
return resp.response(), nil
|
||||
}
|
||||
|
||||
resp, err := c.RoundTripper.RoundTrip(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.kv.Set(r.Context(), req.cacheK(), newCacheableResponse(resp).cacheV())
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
type cacheableRequest struct {
|
||||
URLHost string
|
||||
URLPath string
|
||||
URLQuery cacheableHTTPHeader
|
||||
Header cacheableHTTPHeader
|
||||
Body string
|
||||
}
|
||||
|
||||
func newCacheableRequest(r *http.Request) cacheableRequest {
|
||||
defer r.Body.Close()
|
||||
b, _ := io.ReadAll(r.Body)
|
||||
r.Body = io.NopCloser(bytes.NewReader(b))
|
||||
return cacheableRequest{
|
||||
URLHost: r.URL.Host,
|
||||
URLPath: r.URL.Path,
|
||||
URLQuery: newCacheableHTTPHeader(r.URL.Query()),
|
||||
Header: newCacheableHTTPHeader(r.Header),
|
||||
Body: string(b),
|
||||
}
|
||||
}
|
||||
|
||||
func (c cacheableRequest) cacheK() string {
|
||||
return fmt.Sprint(c)
|
||||
}
|
||||
|
||||
type cacheableResponse struct {
|
||||
Code int
|
||||
Header cacheableHTTPHeader
|
||||
Body string
|
||||
}
|
||||
|
||||
func newCacheableResponse(resp *http.Response) cacheableResponse {
|
||||
defer resp.Body.Close()
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
resp.Body = io.NopCloser(bytes.NewReader(b))
|
||||
return cacheableResponse{
|
||||
Code: resp.StatusCode,
|
||||
Header: newCacheableHTTPHeader(resp.Header),
|
||||
Body: string(b),
|
||||
}
|
||||
}
|
||||
|
||||
func parseCacheableResponse(b []byte) *cacheableResponse {
|
||||
var c cacheableResponse
|
||||
if err := json.Unmarshal(b, &c); err != nil {
|
||||
return nil
|
||||
}
|
||||
return &c
|
||||
}
|
||||
|
||||
func (c cacheableResponse) cacheV() []byte {
|
||||
b, _ := json.Marshal(c)
|
||||
return b
|
||||
}
|
||||
|
||||
func (c cacheableResponse) response() *http.Response {
|
||||
return &http.Response{
|
||||
StatusCode: c.Code,
|
||||
Header: c.Header.header(),
|
||||
Body: io.NopCloser(bytes.NewReader([]byte(c.Body))),
|
||||
}
|
||||
}
|
||||
|
||||
type cacheableHTTPHeader [][]string
|
||||
|
||||
func newCacheableHTTPHeader(m map[string][]string) cacheableHTTPHeader {
|
||||
result := [][]string{}
|
||||
for k, v := range m {
|
||||
result = append(result, append([]string{k}, v...))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (c cacheableHTTPHeader) header() http.Header {
|
||||
return http.Header(c.m())
|
||||
}
|
||||
|
||||
func (c cacheableHTTPHeader) query() url.Values {
|
||||
return url.Values(c.m())
|
||||
}
|
||||
|
||||
func (c cacheableHTTPHeader) m() map[string][]string {
|
||||
m := map[string][]string{}
|
||||
for _, v := range c {
|
||||
v := v
|
||||
m[v[0]] = v[1:]
|
||||
}
|
||||
return m
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package with_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.bel.blue/bel/with"
|
||||
)
|
||||
|
||||
func TestCachedHTTP(t *testing.T) {
|
||||
called := false
|
||||
code := http.StatusAccepted
|
||||
headers := http.Header{
|
||||
"K": []string{"v"},
|
||||
"K2": []string{"v2a", "v2b"},
|
||||
}
|
||||
body := "response body"
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
called = true
|
||||
for k, v := range headers {
|
||||
for _, subv := range v {
|
||||
w.Header().Add(k, subv)
|
||||
}
|
||||
}
|
||||
w.WriteHeader(code)
|
||||
w.Write([]byte(body))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
testResp := func(resp *http.Response) {
|
||||
if resp.StatusCode != code {
|
||||
t.Errorf("initial request wrong status code: %d", resp.StatusCode)
|
||||
} else if !slices.Equal(resp.Header["K"], headers["K"]) {
|
||||
t.Errorf("initial request wrong headers[k]: %+v in %+v", resp.Header["K"], resp.Header)
|
||||
} else if !slices.Equal(resp.Header["K2"], headers["K2"]) {
|
||||
t.Errorf("initial request wrong headers[k2]: %+v in %+v", resp.Header["K2"], resp.Header)
|
||||
} else if b, _ := io.ReadAll(resp.Body); string(b) != body {
|
||||
t.Errorf("initial request wrong resp body: %q", b)
|
||||
}
|
||||
}
|
||||
|
||||
if err := with.CachedHTTP(context.Background(), func(c *http.Client) error {
|
||||
req := func() *http.Request {
|
||||
req, _ := http.NewRequest(http.MethodGet, server.URL+"/my/path", strings.NewReader("my body"))
|
||||
return req
|
||||
}
|
||||
|
||||
if resp, err := c.Do(req()); err != nil {
|
||||
t.Fatalf("failed initial request: %v", err)
|
||||
} else if !called {
|
||||
t.Errorf("initial request didnt hit server")
|
||||
} else {
|
||||
testResp(resp)
|
||||
}
|
||||
|
||||
called = false
|
||||
if resp, err := c.Do(req()); err != nil {
|
||||
t.Fatalf("failed second request: %v", err)
|
||||
} else if called {
|
||||
t.Errorf("second request didnt hit cache")
|
||||
} else {
|
||||
testResp(resp)
|
||||
}
|
||||
|
||||
called = false
|
||||
reqDiffURLPath := req()
|
||||
reqDiffURLPath.URL.Path += "/teehee"
|
||||
if resp, err := c.Do(reqDiffURLPath); err != nil {
|
||||
t.Fatalf("failed diff url path request: %v", err)
|
||||
} else if !called {
|
||||
t.Errorf("new initial hit cache")
|
||||
} else {
|
||||
testResp(resp)
|
||||
}
|
||||
|
||||
called = false
|
||||
reqDiffURLQuery := req()
|
||||
reqDiffURLQuery.URL.RawQuery += "hello=world"
|
||||
if resp, err := c.Do(reqDiffURLQuery); err != nil {
|
||||
t.Fatalf("failed diff url query request: %v", err)
|
||||
} else if !called {
|
||||
t.Errorf("new initial hit cache")
|
||||
} else {
|
||||
testResp(resp)
|
||||
}
|
||||
|
||||
called = false
|
||||
reqDiffHeader := req()
|
||||
reqDiffHeader.Header.Set("Misc", "Misc")
|
||||
if resp, err := c.Do(reqDiffHeader); err != nil {
|
||||
t.Fatalf("failed diff header request: %v", err)
|
||||
} else if !called {
|
||||
t.Errorf("new initial hit cache")
|
||||
} else {
|
||||
testResp(resp)
|
||||
}
|
||||
|
||||
called = false
|
||||
reqDiffBody := req()
|
||||
reqDiffBody.Body = io.NopCloser(strings.NewReader("diff"))
|
||||
reqDiffBody.ContentLength = int64(len("diff"))
|
||||
if resp, err := c.Do(reqDiffBody); err != nil {
|
||||
t.Fatalf("failed diff body request: %v", err)
|
||||
} else if !called {
|
||||
t.Errorf("new initial hit cache")
|
||||
} else {
|
||||
testResp(resp)
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package with
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
type SQLKV struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func KV(ctx context.Context, db *sql.DB, foo func(SQLKV) error) error {
|
||||
if _, err := db.ExecContext(ctx, `
|
||||
CREATE TABLE IF NOT EXISTS with_kv(
|
||||
k TEXT PRIMARY KEY
|
||||
, v TEXT
|
||||
)
|
||||
`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return foo(SQLKV{db: db})
|
||||
}
|
||||
|
||||
func (kv SQLKV) Get(ctx context.Context, k string) ([]byte, error) {
|
||||
row := kv.db.QueryRowContext(ctx, `
|
||||
SELECT v FROM with_kv WHERE k=$1
|
||||
`, k)
|
||||
|
||||
var v []byte
|
||||
if err := row.Scan(&v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return v, row.Err()
|
||||
}
|
||||
|
||||
func (kv SQLKV) Set(ctx context.Context, k string, v []byte) error {
|
||||
_, err := kv.db.ExecContext(ctx, `
|
||||
INSERT INTO with_kv
|
||||
(k, v) VALUES ($1, $2)
|
||||
ON CONFLICT DO UPDATE
|
||||
SET v=$2 WHERE k=$1
|
||||
`, k, v)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package with_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
"gitea.bel.blue/bel/with"
|
||||
)
|
||||
|
||||
func TestKV(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
if err := with.Sqlite(ctx, ":memory:", func(db *sql.DB) error {
|
||||
return with.KV(ctx, db, func(kv with.SQLKV) error {
|
||||
if _, err := kv.Get(ctx, "k"); err == nil {
|
||||
t.Error("404 no err")
|
||||
}
|
||||
|
||||
if err := kv.Set(ctx, "k", []byte("v")); err != nil {
|
||||
t.Error("err on insert", err)
|
||||
}
|
||||
|
||||
if v, err := kv.Get(ctx, "k"); err != nil {
|
||||
t.Error("err on get", err)
|
||||
} else if string(v) != "v" {
|
||||
t.Errorf("expected 'v' but got %q", v)
|
||||
}
|
||||
|
||||
if err := kv.Set(ctx, "k", []byte("v2")); err != nil {
|
||||
t.Error("err on update", err)
|
||||
}
|
||||
|
||||
if v, err := kv.Get(ctx, "k"); err != nil {
|
||||
t.Error("err on get updated", err)
|
||||
} else if string(v) != "v2" {
|
||||
t.Errorf("expected 'v2' but got %q", v)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package with
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
func PSQL(ctx context.Context, conn string, foo func(*sql.DB) error) error {
|
||||
return _sql(ctx, "postgres", conn, foo)
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package with
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
func SQL(ctx context.Context, conn string, foo func(*sql.DB) error) error {
|
||||
u, err := url.Parse(conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch u.Scheme {
|
||||
case "sqlite":
|
||||
return Sqlite(ctx, conn, foo)
|
||||
case "postgres", "postgresql":
|
||||
return PSQL(ctx, conn, foo)
|
||||
}
|
||||
return fmt.Errorf("unknown sql scheme %q", u.Scheme)
|
||||
}
|
||||
|
||||
func _sql(ctx context.Context, engine, conn string, foo func(db *sql.DB) error) error {
|
||||
log.Printf("opening %s %s...", engine, conn)
|
||||
db, err := sql.Open(engine, conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
log.Println("closed:", db.Close())
|
||||
}()
|
||||
|
||||
if err := Timeout(ctx, 10*time.Second, func(ctx context.Context) error {
|
||||
pinged := make(chan bool)
|
||||
defer close(pinged)
|
||||
for ctx.Err() == nil {
|
||||
log.Println("pinging...")
|
||||
go func() {
|
||||
defer func() { recover() }()
|
||||
err := db.PingContext(ctx)
|
||||
if err != nil {
|
||||
log.Println("!", err)
|
||||
}
|
||||
ok := err == nil
|
||||
select {
|
||||
case pinged <- ok:
|
||||
case <-ctx.Done():
|
||||
case <-time.After(time.Second * 5):
|
||||
}
|
||||
}()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break
|
||||
case ok := <-pinged:
|
||||
if ok {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-time.After(time.Second):
|
||||
}
|
||||
}
|
||||
return ctx.Err()
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Println("connected")
|
||||
|
||||
return foo(db)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package with
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
func Sqlite(ctx context.Context, conn string, foo func(db *sql.DB) error) error {
|
||||
conn = strings.TrimPrefix(conn, "sqlite://")
|
||||
return _sql(ctx, "sqlite", conn, func(db *sql.DB) error {
|
||||
if err := Timeout(ctx, 2*time.Second, func(ctx context.Context) error {
|
||||
_, err := db.ExecContext(ctx, `PRAGMA journal_mode=WAL`)
|
||||
return err
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return foo(db)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package with_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"gitea.bel.blue/bel/with"
|
||||
)
|
||||
|
||||
func TestSqlite(t *testing.T) {
|
||||
d := t.TempDir()
|
||||
myErr := fmt.Errorf("my err")
|
||||
if err := with.Sqlite(context.Background(), "sqlite://"+path.Join(d, "db"), func(db *sql.DB) error {
|
||||
return myErr
|
||||
}); err != myErr {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
FROM golang:1.26-trixie AS builder
|
||||
WORKDIR /go/src/
|
||||
COPY ./go.* ./
|
||||
COPY ./cmd ./cmd/
|
||||
COPY ./.vendor ./.vendor/
|
||||
ENV CGO_ENABLED=0
|
||||
RUN go build \
|
||||
-o /go/bin/binary \
|
||||
-mod=readonly \
|
||||
-ldflags="-extldflags '-static'" \
|
||||
./cmd/*/
|
||||
FROM debian:trixie-slim AS runtime
|
||||
COPY --from=builder /go/bin/binary /bin/binary
|
||||
CMD []
|
||||
ENTRYPOINT ["/bin/binary"]
|
||||
@@ -1,19 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"gitea.bel.blue/bel/with"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := with.Context(run); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func run(ctx context.Context) error {
|
||||
log.Fatal("RUN")
|
||||
return ctx.Err()
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<header>
|
||||
<script>
|
||||
function onload() {
|
||||
const results = [
|
||||
{{ range $v := query
|
||||
`
|
||||
SELECT
|
||||
start AS start
|
||||
, CASE
|
||||
WHEN ms < 50 AND ok THEN
|
||||
'okay'
|
||||
WHEN ms < 750 AND ok THEN
|
||||
'slow'
|
||||
ELSE
|
||||
'bad'
|
||||
END AS ok
|
||||
, ms AS ms
|
||||
FROM
|
||||
"pinger_pings"
|
||||
ORDER BY start DESC
|
||||
LIMIT 1000
|
||||
`
|
||||
-}}
|
||||
[
|
||||
{{.ok}}==="okay"
|
||||
? "🟢"
|
||||
: {{.ok}}==="slow"
|
||||
? "🟠"
|
||||
: "🔴",
|
||||
`${Math.round((new Date() - 1000*{{.start}})/1000.0)}s ago`,
|
||||
new Date(1000*{{.start}}),
|
||||
"{{.ms}}ms",
|
||||
],
|
||||
{{ end }}
|
||||
]
|
||||
document.getElementById("draw").innerHTML = results.map((r) => `<br>${JSON.stringify(r)}`).join("\n");
|
||||
}
|
||||
</script>
|
||||
</header>
|
||||
<body onload="onload()">
|
||||
<div id="draw">
|
||||
</div>
|
||||
</body>
|
||||
<footer>
|
||||
</footer>
|
||||
</html>
|
||||
@@ -0,0 +1,261 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"context"
|
||||
"database/sql"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitea.bel.blue/bel/with"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := with.Context(run); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
type S struct {
|
||||
*sql.DB
|
||||
}
|
||||
|
||||
func run(ctx context.Context) error {
|
||||
return with.Sqlite(ctx, "/tmp/pinger.db", func(db *sql.DB) error {
|
||||
s := S{
|
||||
DB: db,
|
||||
}
|
||||
if err := s.init(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
defer wg.Wait()
|
||||
for _, foo := range []func(context.Context) error{
|
||||
s.pingAndRecord,
|
||||
s.collectGarbage,
|
||||
s.listen,
|
||||
} {
|
||||
foo := foo
|
||||
wg.Go(func() {
|
||||
if err := foo(ctx); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
return ctx.Err()
|
||||
})
|
||||
}
|
||||
|
||||
func (s S) init(ctx context.Context) error {
|
||||
for _, q := range []string{
|
||||
`CREATE TABLE IF NOT EXISTS "pinger_to" (
|
||||
id NUMBER NOT NULL UNIQUE
|
||||
, addr TEXT NOT NULL UNIQUE
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS "pinger_pings" (
|
||||
start TIMESTAMP NOT NULL
|
||||
, ms NUMBER NOT NULL
|
||||
, ok BOOLEAN NOT NULL DEFAULT false
|
||||
, to_id NUMBER
|
||||
, FOREIGN KEY (to_id) REFERENCES "pinger_to"(id)
|
||||
)`,
|
||||
//`ALTER TABLE "pinger_pings" SET UNLOGGED`,
|
||||
} {
|
||||
if _, err := s.ExecContext(ctx, q); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
//go:embed index.html.tmpl
|
||||
var IndexTemplate string
|
||||
|
||||
func (s S) listen(ctx context.Context) error {
|
||||
t := template.New("template")
|
||||
t.Funcs(map[string]any{
|
||||
"query": func(q string) ([]map[string]any, error) {
|
||||
rows, err := s.QueryContext(ctx, q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
columns, err := rows.Columns()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results := []map[string]any{}
|
||||
for rows.Next() {
|
||||
var scanners []any
|
||||
for range columns {
|
||||
var a any
|
||||
scanners = append(scanners, &a)
|
||||
}
|
||||
if err := rows.Scan(scanners...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := map[string]any{}
|
||||
for i, column := range columns {
|
||||
result[column] = scanners[i]
|
||||
}
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
return results, rows.Err()
|
||||
},
|
||||
})
|
||||
t, err := t.Parse(IndexTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
server := &http.Server{
|
||||
Addr: ":10000",
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := t.Execute(w, map[string]any{
|
||||
"DB": s.DB,
|
||||
}); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}),
|
||||
}
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
server.Close()
|
||||
}()
|
||||
|
||||
if err := server.ListenAndServe(); err != nil && ctx.Err() == nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s S) collectGarbage(ctx context.Context) error {
|
||||
c := time.NewTicker(time.Hour)
|
||||
defer c.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.C:
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
}
|
||||
if _, err := s.ExecContext(ctx, `
|
||||
DELETE FROM "pinger_pings"
|
||||
WHERE
|
||||
start < $1
|
||||
AND (
|
||||
ms < 100
|
||||
OR ok
|
||||
)
|
||||
`, time.Now().Add(-1*2*24*time.Hour).Unix()); err != nil {
|
||||
log.Println("!", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s S) pingAndRecord(ctx context.Context) error {
|
||||
ctx, can := context.WithCancel(ctx)
|
||||
defer can()
|
||||
|
||||
fs := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
|
||||
target := fs.String("t", "google.com", "target")
|
||||
port := fs.String("p", "80", "target port")
|
||||
interval := fs.Duration("i", time.Second, "between conns")
|
||||
if err := fs.Parse(os.Args[1:]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%s", *target, *port)
|
||||
if _, err := s.ExecContext(ctx, `
|
||||
INSERT INTO "pinger_to" (id, addr)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (addr) DO NOTHING
|
||||
`, rand.Int(), addr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var toId int
|
||||
if row := s.QueryRowContext(ctx, `
|
||||
SELECT id
|
||||
FROM "pinger_to"
|
||||
WHERE addr=$1
|
||||
`, addr); row.Err() != nil {
|
||||
return row.Err()
|
||||
} else if err := row.Scan(&toId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type record struct {
|
||||
start time.Time
|
||||
ms time.Duration
|
||||
ok bool
|
||||
}
|
||||
records := make(chan record, 100)
|
||||
defer close(records)
|
||||
go func() {
|
||||
for record := range records {
|
||||
if _, err := s.ExecContext(ctx, `
|
||||
INSERT INTO "pinger_pings"
|
||||
(start, ms, ok, to_id)
|
||||
VALUES
|
||||
($1, $2, $3, $4)
|
||||
`, record.start.Unix(), record.ms.Milliseconds(), record.ok, toId); err != nil {
|
||||
log.Println("!", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
c := time.NewTicker(*interval)
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
c.Stop()
|
||||
}()
|
||||
|
||||
dialer := &net.Dialer{
|
||||
Timeout: time.Second,
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.C:
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
}
|
||||
func() {
|
||||
ctx, can := context.WithTimeout(ctx, *interval)
|
||||
defer can()
|
||||
start := time.Now()
|
||||
conn, err := dialer.DialContext(ctx, "tcp", addr)
|
||||
select {
|
||||
case records <- record{
|
||||
start: start,
|
||||
ms: time.Since(start),
|
||||
ok: err == nil,
|
||||
}:
|
||||
default:
|
||||
}
|
||||
if err == nil {
|
||||
conn.Close()
|
||||
} else {
|
||||
log.Println("!", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return ctx.Err()
|
||||
}
|
||||
@@ -2,18 +2,25 @@ module gitea/lobby-irl
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require gitea.bel.blue/bel/with v0.0.0-20260527141310-eadcabb7fe54
|
||||
|
||||
require (
|
||||
gitea.bel.blue/bel/with v0.0.0-20260527141310-eadcabb7fe54 // indirect
|
||||
github.com/digineo/go-logwrap v0.0.0-20181106161722-a178c58ea3f0 // indirect
|
||||
github.com/digineo/go-ping v1.2.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/lib/pq v1.11.2 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e // indirect
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
modernc.org/libc v1.67.6 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.46.1 // indirect
|
||||
)
|
||||
|
||||
replace gitea.bel.blue/bel/with => ./.vendor/with
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
gitea.bel.blue/bel/with v0.0.0-20260527141310-eadcabb7fe54 h1:4a3/ryr2O4qlKebPJ+hrXQzmCXsQ3u2AE0FbCtkM8Wk=
|
||||
gitea.bel.blue/bel/with v0.0.0-20260527141310-eadcabb7fe54/go.mod h1:9/uv9/wJdSRErentSOb2flc8rgnRXlOIdOUdaVPUl/c=
|
||||
github.com/digineo/go-logwrap v0.0.0-20181106161722-a178c58ea3f0 h1:OT/LKmj81wMymnWXaKaKBR9n1vPlu+GC0VVKaZP6kzs=
|
||||
github.com/digineo/go-logwrap v0.0.0-20181106161722-a178c58ea3f0/go.mod h1:DmqdumeAKGQNU5E8MN0ruT5ZGx8l/WbAsMbXCXcSEts=
|
||||
github.com/digineo/go-ping v1.2.0 h1:/9vEsoCRtQvol5vRMA2pE8guuhUVDSJB2ok2nKuJJbA=
|
||||
github.com/digineo/go-ping v1.2.0/go.mod h1:cXJTVTs7mthQ41c/nWykPYuhlDQwCN0ba5LyOnyYv8Y=
|
||||
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/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
|
||||
github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
@@ -12,16 +18,46 @@ github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOF
|
||||
github.com/ncruces/go-strftime v1.0.0/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=
|
||||
github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e h1:nt2877sKfojlHCTOBXbpWjBkuWKritFaGIfgQwbQUls=
|
||||
github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e/go.mod h1:B4+Kq1u5FlULTjFSM707Q6e/cOHFv0z/6QRoxubDIQ8=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
|
||||
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
|
||||
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
|
||||
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
|
||||
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
|
||||
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.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/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.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
|
||||
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
||||
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=
|
||||
|
||||
Reference in New Issue
Block a user