diff options
| author | Paul Buetow <paul@buetow.org> | 2026-07-22 23:51:18 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-07-22 23:51:18 +0300 |
| commit | 849951be1d1a7ee9f9302006ccb187bf5b4e36f3 (patch) | |
| tree | 496c924a03a9ea6212e29bb4699e268066ebad81 /internal/server/continuous_test.go | |
| parent | bf78b3abffee6d49c08ca2980156afc455994969 (diff) | |
feat: DTail fork — server/client feature development
Squashed development of the snonux/dtail fork's product code (internal/, cmd/)
since diverging from mimecast/dtail. Major areas:
- Read/output path: the former "turbo" channel-less path is now the single,
default server-side read/output path for cat/grep/tail and MapReduce; the old
channel-based path and its config/env toggles were removed.
- MapReduce: single aggregate implementation (server + serverless) fed directly
by a processor pipeline, with input-exhausted finalization via the shutdown
coordinator; high-concurrency and data-race fixes.
- Journal source reads (journal:unit.service) via journalctl, Linux-gated behind
a journal-v1 capability.
- Auth-key fast reconnect: in-memory per-user public-key cache with TTL/max-keys,
registered over an authenticated session (AUTHKEY), checked before
authorized_keys.
- Interactive query reload (--interactive-query) with SESSION START/UPDATE
generation boundaries and capability negotiation.
- Client-side deadlines: --timeout / --shutdownAfter as context deadlines;
follow shutdown handling.
- Client logging: diagnostics-only daily log by default, opt-in payload tee via
--log-payload.
- Numerous correctness fixes (buffer-pool double-recycle races, EOF-sentinel
leaks, glob-expansion cap, TOCTOU in CSV parsing) with accompanying unit tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'internal/server/continuous_test.go')
| -rw-r--r-- | internal/server/continuous_test.go | 202 |
1 files changed, 202 insertions, 0 deletions
diff --git a/internal/server/continuous_test.go b/internal/server/continuous_test.go new file mode 100644 index 0000000..12d9f18 --- /dev/null +++ b/internal/server/continuous_test.go @@ -0,0 +1,202 @@ +package server + +import ( + "context" + "sync/atomic" + "testing" + "time" + + "github.com/mimecast/dtail/internal/clients" + "github.com/mimecast/dtail/internal/config" + "github.com/mimecast/dtail/internal/io/dlog" +) + +func TestSameCalendarDay(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + a time.Time + b time.Time + want bool + }{ + { + name: "same day", + a: time.Date(2026, time.January, 15, 10, 0, 0, 0, time.UTC), + b: time.Date(2026, time.January, 15, 23, 59, 59, 0, time.UTC), + want: true, + }, + { + name: "same day-of-month in different months", + a: time.Date(2026, time.January, 15, 10, 0, 0, 0, time.UTC), + b: time.Date(2026, time.February, 15, 10, 0, 0, 0, time.UTC), + want: false, + }, + { + name: "same day-of-month across years", + a: time.Date(2025, time.December, 31, 10, 0, 0, 0, time.UTC), + b: time.Date(2026, time.January, 31, 10, 0, 0, 0, time.UTC), + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + if got := sameCalendarDay(tt.a, tt.b); got != tt.want { + t.Fatalf("sameCalendarDay(%v, %v) = %v, want %v", tt.a, tt.b, got, tt.want) + } + }) + } +} + +func TestContinuousRunJobsReleasesDayChangeWatcherAcrossRetries(t *testing.T) { + dlog.Server = &dlog.DLog{} + + c := newContinuous(config.RuntimeConfig{ + Server: &config.ServerConfig{ + SSHBindAddress: "127.0.0.1", + }, + }) + c.retryInterval = 25 * time.Millisecond + + var watcherStarts int32 + var watcherExits int32 + started := make(chan struct{}, 1) + release := make(chan struct{}, 1) + c.newMaprClient = func(args config.Args, mode clients.MaprClientMode) (continuousClient, error) { + return blockingContinuousClient{ + started: started, + release: release, + }, nil + } + c.dayChangeWatcher = func(ctx context.Context) bool { + atomic.AddInt32(&watcherStarts, 1) + defer atomic.AddInt32(&watcherExits, 1) + return c.waitForDayChange(ctx) + } + + job := config.Continuous{} + job.Enable = true + job.RestartOnDayChange = true + c.cfg.Server.Continuous = []config.Continuous{job} + + ctx, cancel := context.WithCancel(context.Background()) + done := make(chan struct{}) + go func() { + c.runJobs(ctx) + close(done) + }() + + for i := int32(1); i <= 5; i++ { + select { + case <-started: + case <-time.After(2 * time.Second): + t.Fatalf("timed out waiting for retry %d to start", i) + } + + waitForCounterAtLeast(t, func() int32 { + return atomic.LoadInt32(&watcherStarts) + }, i) + + release <- struct{}{} + + waitForCounterAtLeast(t, func() int32 { + return atomic.LoadInt32(&watcherExits) + }, i) + } + + cancel() + + select { + case <-done: + case <-time.After(2 * time.Second): + t.Fatal("continuous job runner did not stop after cancellation") + } +} + +func TestContinuousWaitForDayChangeDetectsMonthBoundary(t *testing.T) { + dlog.Server = &dlog.DLog{} + + c := newContinuous(config.RuntimeConfig{}) + + start := time.Date(2026, time.January, 31, 23, 59, 59, 0, time.UTC) + sameDay := time.Date(2026, time.January, 31, 23, 59, 59, 500_000_000, time.UTC) + nextDay := time.Date(2026, time.February, 1, 0, 0, 0, 0, time.UTC) + + var nowCalls int32 + c.now = func() time.Time { + switch atomic.AddInt32(&nowCalls, 1) { + case 1: + return start + case 2: + return sameDay + default: + return nextDay + } + } + + tickCh := make(chan time.Time, 2) + c.newTicker = func(time.Duration) (<-chan time.Time, func()) { + return tickCh, func() {} + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + result := make(chan bool, 1) + go func() { + result <- c.waitForDayChange(ctx) + }() + + tickCh <- start + select { + case got := <-result: + t.Fatalf("waitForDayChange returned after same-day tick: %v", got) + case <-time.After(100 * time.Millisecond): + } + + tickCh <- nextDay + select { + case got := <-result: + if !got { + t.Fatal("waitForDayChange returned false after the month boundary tick") + } + case <-time.After(2 * time.Second): + t.Fatal("timed out waiting for waitForDayChange to detect the month boundary") + } +} + +type blockingContinuousClient struct { + started chan<- struct{} + release <-chan struct{} +} + +func (f blockingContinuousClient) Start(context.Context, <-chan string) int { + f.started <- struct{}{} + <-f.release + return 0 +} + +func waitForCounterAtLeast(t *testing.T, current func() int32, min int32) { + t.Helper() + + deadline := time.NewTimer(2 * time.Second) + defer deadline.Stop() + + ticker := time.NewTicker(10 * time.Millisecond) + defer ticker.Stop() + + for { + if current() >= min { + return + } + + select { + case <-deadline.C: + t.Fatalf("timed out waiting for counter to reach %d, got %d", min, current()) + case <-ticker.C: + } + } +} |
