mirror of
https://github.com/marcusDenslow/terminalShop.git
synced 2026-10-25 02:52:20 +02:00
Compare commits
2 Commits
5dd196834f
...
530e4ec219
| Author | SHA1 | Date | |
|---|---|---|---|
| 530e4ec219 | |||
| 64b6f0d91a |
83
api/main.go
83
api/main.go
@@ -25,6 +25,25 @@ import (
|
||||
|
||||
const version = "v0.1.0"
|
||||
|
||||
// everyTick runs fn on its own goroutine every d until ctx is cancelled.
|
||||
// Shared by the reconciler sweeps so the ticker + shutdown plumbing lives in
|
||||
// one place. ReconcileUnshipped intentionally does not use it, it needs a
|
||||
// staggered warm-up run before its first tick.
|
||||
func everyTick(ctx context.Context, d time.Duration, fn func(context.Context)) {
|
||||
go func() {
|
||||
ticker := time.NewTicker(d)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
fn(ctx)
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func main() {
|
||||
observability.InitLogger()
|
||||
middleware.SetBuildInfo(version)
|
||||
@@ -106,20 +125,8 @@ func main() {
|
||||
}
|
||||
}()
|
||||
|
||||
// Start reconciliation job. runs every 5 minutes to catch any orders
|
||||
// that were charged but not recorded due to a crash.
|
||||
go func() {
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
handlers.ReconcileOrders(ctx)
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
// Catch orders charged but not recorded due to a crash. Every 5 minutes
|
||||
everyTick(ctx, 5*time.Minute, handlers.ReconcileOrders)
|
||||
|
||||
go func() {
|
||||
select {
|
||||
@@ -142,46 +149,18 @@ func main() {
|
||||
|
||||
// Sweep saved cards whose retention deadline elapsed. Read paths filter
|
||||
// expired rows from query results, so this sweeper only drives the Stripe
|
||||
// detach + audit + physical row delete asynchronously.
|
||||
go func() {
|
||||
ticker := time.NewTicker(1 * time.Hour)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
handlers.ReconcileExpiredCards(ctx)
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
// detach + audit + physical row delete asynchronously. Hourly.
|
||||
everyTick(ctx, 1*time.Hour, handlers.ReconcileExpiredCards)
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(15 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
threshold := time.Duration(cfg.Abandoned3DSThresholdMinutes) * time.Minute
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
handlers.ReconcileStale3DSOrders(ctx, threshold)
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
// Flip abandoned 3DS orders to failed. Every 15 minutes.
|
||||
threshold := time.Duration(cfg.Abandoned3DSThresholdMinutes) * time.Minute
|
||||
everyTick(ctx, 15*time.Minute, func(ctx context.Context) {
|
||||
handlers.ReconcileStale3DSOrders(ctx, threshold)
|
||||
})
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(10 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
handlers.ReconcilePayRedirects(ctx)
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
// Sweep expired pay_redirects rows so the table stays bounded. Lazy delete
|
||||
// on read is the primary path; this only reclaims tokens nobody follows.
|
||||
everyTick(ctx, 10*time.Minute, handlers.ReconcilePayRedirects)
|
||||
|
||||
log.Println("API server started. Press Ctrl+C to shutdown.")
|
||||
|
||||
|
||||
90
api/main_test.go
Normal file
90
api/main_test.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestEveryTick_CallsFnOnTick verifies the helper invokes fn repeatedly on its
|
||||
// interval. A non-blocking send keeps the spawned goroutine from ever blocking
|
||||
// on a full channel, and the 1s timeout per tick is generous vs the 1ms
|
||||
// interval so the test in not timing-flaky.
|
||||
func TestEveryTick_CallsFnOnTick(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
fired := make(chan struct{}, 1)
|
||||
everyTick(ctx, time.Millisecond, func(context.Context) {
|
||||
select {
|
||||
case fired <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
})
|
||||
|
||||
for i := range 3 {
|
||||
select {
|
||||
case <-fired:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatalf("tick %d did not fire within 1s", i+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestEveryTick_StopsOnContextCancel verifies cancelling ctx stops the loop.
|
||||
// Once cancel propagates the call count must stop growing. This is the property
|
||||
// that lets the reconciler goroutines exit on graceful shutdown
|
||||
func TestEveryTick_StopsOnContextCancel(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
var calls int64
|
||||
started := make(chan struct{}, 1)
|
||||
everyTick(ctx, time.Millisecond, func(context.Context) {
|
||||
atomic.AddInt64(&calls, 1)
|
||||
select {
|
||||
case started <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
})
|
||||
|
||||
// Wait until the loop is actually running before cancelling.
|
||||
select {
|
||||
case <-started:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatalf("everyTick never called fn")
|
||||
}
|
||||
|
||||
cancel()
|
||||
time.Sleep(20 * time.Millisecond) // let cancel land + any in-flight tick finish
|
||||
stable := atomic.LoadInt64(&calls)
|
||||
|
||||
time.Sleep(30 * time.Millisecond) // 30 more ticks would land if still running
|
||||
if final := atomic.LoadInt64(&calls); final != stable {
|
||||
t.Fatalf("everyTick kept calling fn after cancel: %d -> %d", stable, final)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEveryTick_PassesContextToFn(t *testing.T) {
|
||||
type ctxKey string
|
||||
const marker ctxKey = "marker"
|
||||
ctx, cancel := context.WithCancel(context.WithValue(context.Background(), marker, "present"))
|
||||
defer cancel()
|
||||
|
||||
got := make(chan string, 1)
|
||||
everyTick(ctx, time.Millisecond, func(c context.Context) {
|
||||
v, _ := c.Value(marker).(string)
|
||||
select {
|
||||
case got <- v:
|
||||
default:
|
||||
}
|
||||
})
|
||||
|
||||
select {
|
||||
case v := <-got:
|
||||
if v != "present" {
|
||||
t.Fatalf("fn received wrong context: value = %q, want %q", v, "present")
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatalf("fn never ran")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user