Compare commits

...

15 Commits

Author SHA1 Message Date
bel 439c481c60 log err
cicd / ci (push) Successful in 3m25s
2026-07-07 17:16:08 -06:00
bel 0a58f85ca3 2d cause only reaps sad
cicd / ci (push) Successful in 2m56s
2026-06-28 09:42:07 -06:00
bel 141c0e5100 forget it 30d
cicd / ci (push) Successful in 3m12s
2026-06-28 09:35:43 -06:00
bel 9f3de791f9 fix killnig and 55h 2026-06-28 09:35:22 -06:00
bel 8d3db70e01 botched
cicd / ci (push) Has been cancelled
2026-06-28 09:33:04 -06:00
bel 831fed82f7 go delete success from more than 12h ago
cicd / ci (push) Successful in 3m39s
2026-06-28 09:23:11 -06:00
bel 43887bb447 list ms too
cicd / ci (push) Successful in 2m3s
2026-06-28 00:56:29 -06:00
bel 0b9b790986 okay,slow,err
cicd / ci (push) Successful in 2m51s
2026-06-28 00:51:16 -06:00
bel ae54bee23b dockerfile
cicd / ci (push) Successful in 3m14s
2026-06-28 00:28:19 -06:00
bel 84a8897bf0 it is techincally a ui 2026-06-28 00:27:14 -06:00
bel 2e339cff36 execute template w queries
cicd / ci (push) Failing after 36s
2026-06-28 00:14:25 -06:00
bel 52c00adfe7 pings record to sqlite 2026-06-27 23:59:55 -06:00
bel 1be18b80d2 digineo req superuser 2026-06-27 17:35:15 -06:00
bel faae75d5f4 gitea
cicd / ci (push) Failing after 41s
2026-06-27 16:44:56 -06:00
bel a9f78655c3 vend with 2026-06-27 16:44:47 -06:00
25 changed files with 1439 additions and 22 deletions
+35
View File
@@ -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
+32
View File
@@ -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
}
+31
View File
@@ -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)
})
}
+24
View File
@@ -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)
}
+75
View File
@@ -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
}
+122
View File
@@ -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)
}
})
}
+103
View File
@@ -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()
}
+34
View File
@@ -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()
}
}
+31
View File
@@ -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
)
+89
View File
@@ -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=
+140
View File
@@ -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
}
+119
View File
@@ -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)
}
}
+46
View File
@@ -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
}
+44
View File
@@ -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)
}
}
+12
View File
@@ -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)
}
+76
View File
@@ -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)
}
+23
View File
@@ -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)
})
}
+21
View File
@@ -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
View File
@@ -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"]
-19
View File
@@ -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()
}
+48
View File
@@ -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>
+261
View File
@@ -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()
}
+8 -1
View File
@@ -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
+38 -2
View File
@@ -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=