Skip to content

Commit 25f5131

Browse files
committed
speed up integration tests
1 parent e53010a commit 25f5131

6 files changed

Lines changed: 472 additions & 100 deletions

File tree

.github/workflows/lint-test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ jobs:
7575
run: docker compose logs --tail 100 nginx nginx2 traefik && docker compose down
7676
working-directory: ./ci
7777

78-
integration-test-backwards-compatibility:
79-
needs: [integration-test]
78+
integration-test:
79+
needs: [integration-test-latest]
8080
permissions:
8181
contents: read
8282
runs-on: ubuntu-24.04

ci/docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ services:
2121
traefik.http.middlewares.captcha-protect.plugin.captcha-protect.goodBots: ""
2222
traefik.http.middlewares.captcha-protect.plugin.captcha-protect.protectRoutes: "/"
2323
traefik.http.middlewares.captcha-protect.plugin.captcha-protect.persistentStateFile: "/tmp/state.json"
24+
traefik.http.middlewares.captcha-protect.plugin.captcha-protect.enableStateReconciliation: "true"
2425
healthcheck:
2526
test: curl -fs http://localhost/healthz | grep -q OK || exit 1
2627
volumes:

ci/test.go

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -52,26 +52,15 @@ func main() {
5252
fmt.Printf("Making sure %d attempt(s) pass\n", rateLimit)
5353
runParallelChecks(ips, rateLimit, "http://localhost")
5454

55+
time.Sleep(cp.StateSaveInterval + cp.StateSaveJitter + (1 * time.Second))
56+
runCommand("jq", ".", "tmp/state.json")
57+
5558
fmt.Printf("Making sure attempt #%d causes a redirect to the challenge page\n", rateLimit+1)
5659
ensureRedirect(ips, "http://localhost")
5760

5861
fmt.Println("\nTesting state sharing between nginx instances...")
59-
fmt.Println("Waiting 2 seconds for state to save to disk...")
60-
time.Sleep(cp.StateSaveInterval + (5 * time.Second))
6162
testStateSharing(ips)
6263

63-
fmt.Println("Sleeping for 2m")
64-
time.Sleep(125 * time.Second)
65-
fmt.Println("Making sure one attempt passes after 2m window")
66-
runParallelChecks(ips, 1, "http://localhost")
67-
fmt.Println("All good 🚀")
68-
69-
// make sure the state has time to save
70-
fmt.Println("Waiting for state to save")
71-
runCommand("jq", ".", "tmp/state.json")
72-
time.Sleep(cp.StateSaveInterval + (5 * time.Second))
73-
runCommand("jq", ".", "tmp/state.json")
74-
7564
runCommand("docker", "container", "stats", "--no-stream")
7665

7766
// now restart the containers and make sure the previous state reloaded

internal/state/state.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package state
22

33
import (
4+
"encoding/json"
5+
"log/slog"
6+
"os"
47
"reflect"
58
"time"
69

@@ -255,3 +258,105 @@ func ReconcileState(fileState State, rateCache, botCache, verifiedCache *lru.Cac
255258
}
256259
}
257260
}
261+
262+
// SaveStateToFile saves state to a file with locking and optional reconciliation.
263+
// When reconcile is true, it reads and merges existing file state before saving.
264+
// Returns timing metrics for debugging.
265+
func SaveStateToFile(
266+
filePath string,
267+
reconcile bool,
268+
rateCache, botCache, verifiedCache *lru.Cache,
269+
log *slog.Logger,
270+
) (lockMs, readMs, reconcileMs, marshalMs, writeMs, totalMs int64, err error) {
271+
startTime := time.Now()
272+
273+
lock, err := NewFileLock(filePath + ".lock")
274+
if err != nil {
275+
return 0, 0, 0, 0, 0, 0, err
276+
}
277+
defer lock.Close()
278+
279+
if err := lock.Lock(); err != nil {
280+
return 0, 0, 0, 0, 0, 0, err
281+
}
282+
lockDuration := time.Since(startTime)
283+
284+
var readDuration, reconcileDuration, marshalDuration, writeDuration time.Duration
285+
286+
// Reconcile with existing file state if enabled
287+
if reconcile {
288+
readStart := time.Now()
289+
fileContent, readErr := os.ReadFile(filePath)
290+
readDuration = time.Since(readStart)
291+
292+
if readErr == nil && len(fileContent) > 0 {
293+
reconcileStart := time.Now()
294+
var fileState State
295+
if unmarshalErr := json.Unmarshal(fileContent, &fileState); unmarshalErr == nil {
296+
log.Debug("Reconciling state before save", "fileBytes", len(fileContent))
297+
ReconcileState(fileState, rateCache, botCache, verifiedCache)
298+
}
299+
reconcileDuration = time.Since(reconcileStart)
300+
}
301+
}
302+
303+
// Marshal current state
304+
marshalStart := time.Now()
305+
currentState := GetState(rateCache.Items(), botCache.Items(), verifiedCache.Items())
306+
jsonData, err := json.Marshal(currentState)
307+
marshalDuration = time.Since(marshalStart)
308+
309+
if err != nil {
310+
return lockDuration.Milliseconds(), readDuration.Milliseconds(),
311+
reconcileDuration.Milliseconds(), marshalDuration.Milliseconds(),
312+
0, 0, err
313+
}
314+
315+
// Write to disk
316+
writeStart := time.Now()
317+
err = os.WriteFile(filePath, jsonData, 0644)
318+
writeDuration = time.Since(writeStart)
319+
320+
if err != nil {
321+
return lockDuration.Milliseconds(), readDuration.Milliseconds(),
322+
reconcileDuration.Milliseconds(), marshalDuration.Milliseconds(),
323+
writeDuration.Milliseconds(), 0, err
324+
}
325+
326+
totalDuration := time.Since(startTime)
327+
return lockDuration.Milliseconds(), readDuration.Milliseconds(),
328+
reconcileDuration.Milliseconds(), marshalDuration.Milliseconds(),
329+
writeDuration.Milliseconds(), totalDuration.Milliseconds(), nil
330+
}
331+
332+
// LoadStateFromFile loads state from a file with locking.
333+
func LoadStateFromFile(
334+
filePath string,
335+
rateCache, botCache, verifiedCache *lru.Cache,
336+
) error {
337+
lock, err := NewFileLock(filePath + ".lock")
338+
if err != nil {
339+
return err
340+
}
341+
defer lock.Close()
342+
343+
if err := lock.Lock(); err != nil {
344+
return err
345+
}
346+
347+
fileContent, err := os.ReadFile(filePath)
348+
if err != nil || len(fileContent) == 0 {
349+
return err
350+
}
351+
352+
var loadedState State
353+
err = json.Unmarshal(fileContent, &loadedState)
354+
if err != nil {
355+
return err
356+
}
357+
358+
// Use SetState which properly handles expiration times
359+
SetState(loadedState, rateCache, botCache, verifiedCache)
360+
361+
return nil
362+
}

0 commit comments

Comments
 (0)