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
|
go 1.25.0
|
||||||
|
|
||||||
|
require gitea.bel.blue/bel/with v0.0.0-20260527141310-eadcabb7fe54
|
||||||
|
|
||||||
require (
|
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/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/lib/pq v1.11.2 // indirect
|
github.com/lib/pq v1.11.2 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // 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/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||||
|
golang.org/x/net v0.43.0 // indirect
|
||||||
golang.org/x/sys v0.43.0 // indirect
|
golang.org/x/sys v0.43.0 // indirect
|
||||||
modernc.org/libc v1.67.6 // indirect
|
modernc.org/libc v1.67.6 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
modernc.org/sqlite v1.46.1 // 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=
|
github.com/digineo/go-logwrap v0.0.0-20181106161722-a178c58ea3f0 h1:OT/LKmj81wMymnWXaKaKBR9n1vPlu+GC0VVKaZP6kzs=
|
||||||
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/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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
|
||||||
github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
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 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/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 h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
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 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
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 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
|
||||||
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
|
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 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
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 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
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 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
|
||||||
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
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