package notify import ( "b612.me/stario" "context" "errors" "math" "net" "strings" "sync" "sync/atomic" "testing" "time" ) type bulkReleaseTrackingConn struct { net.Conn started chan struct{} once sync.Once } func (c *bulkReleaseTrackingConn) Write(p []byte) (int, error) { c.once.Do(func() { close(c.started) }) return c.Conn.Write(p) } func TestBulkOwnedChunkReleaseAfterRead(t *testing.T) { bulk := newBulkHandle(context.Background(), newBulkRuntime("buffer-release-read"), clientFileScope(), BulkOpenRequest{ BulkID: "buffer-release-read", DataID: 1, }, 0, nil, nil, 0, nil, nil, nil, nil, nil) released := 0 if err := bulk.pushOwnedChunkWithReleaseNoReset([]byte("hello"), func() { released++ }); err != nil { t.Fatalf("pushOwnedChunkWithReleaseNoReset failed: %v", err) } buf := make([]byte, 5) n, err := bulk.Read(buf) if err != nil { t.Fatalf("Read failed: %v", err) } if n != 5 || string(buf[:n]) != "hello" { t.Fatalf("Read = %d %q, want 5 hello", n, string(buf[:n])) } if released != 1 { t.Fatalf("release count = %d, want 1", released) } } func TestBulkOwnedChunkReleaseOnReset(t *testing.T) { bulk := newBulkHandle(context.Background(), newBulkRuntime("buffer-release-reset"), clientFileScope(), BulkOpenRequest{ BulkID: "buffer-release-reset", DataID: 1, }, 0, nil, nil, 0, nil, nil, nil, nil, nil) released := 0 if err := bulk.pushOwnedChunkWithReleaseNoReset([]byte("hello"), func() { released++ }); err != nil { t.Fatalf("pushOwnedChunkWithReleaseNoReset failed: %v", err) } bulk.markReset(errors.New("boom")) if released != 1 { t.Fatalf("release count = %d, want 1", released) } } func TestBulkReadDoesNotBlockOnAsyncWindowRelease(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() releaseStarted := make(chan struct{}) releaseUnblock := make(chan struct{}) bulk := newBulkHandle(ctx, newBulkRuntime("buffer-release-async"), clientFileScope(), BulkOpenRequest{ BulkID: "buffer-release-async", DataID: 1, ChunkSize: 4, WindowBytes: 4, MaxInFlight: 1, }, 0, nil, nil, 0, nil, nil, nil, nil, func(_ *bulkHandle, bytes int64, chunks int) error { if bytes != 4 || chunks != 1 { t.Fatalf("release = (%d,%d), want (4,1)", bytes, chunks) } close(releaseStarted) <-releaseUnblock return nil }) if err := bulk.pushOwnedChunk([]byte("ping")); err != nil { t.Fatalf("pushOwnedChunk failed: %v", err) } buf := make([]byte, 4) doneCh := make(chan error, 1) go func() { n, err := bulk.Read(buf) if err != nil { doneCh <- err return } if got, want := n, 4; got != want { doneCh <- errors.New("unexpected read size") return } doneCh <- nil }() select { case err := <-doneCh: if err != nil { t.Fatalf("Read failed: %v", err) } case <-time.After(200 * time.Millisecond): t.Fatal("Read should not block on async release sender") } select { case <-releaseStarted: case <-time.After(time.Second): t.Fatal("window release sender did not start") } close(releaseUnblock) cancel() if bulk.releaseWorkerDone != nil { select { case <-bulk.releaseWorkerDone: case <-time.After(time.Second): t.Fatal("release worker did not exit") } } } func TestBulkWindowReleaseRetriesTimeoutWithoutLosingCredit(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() var calls atomic.Int32 secondAttempt := make(chan struct{}) bulk := newBulkHandle(ctx, newBulkRuntime("buffer-release-retry"), clientFileScope(), BulkOpenRequest{ BulkID: "buffer-release-retry", DataID: 1, ChunkSize: 4, WindowBytes: 4, MaxInFlight: 1, }, 0, nil, nil, 0, nil, nil, nil, nil, func(_ *bulkHandle, bytes int64, chunks int) error { if bytes != 4 || chunks != 1 { t.Fatalf("release = (%d,%d), want (4,1)", bytes, chunks) } if calls.Add(1) == 1 { return context.DeadlineExceeded } close(secondAttempt) return nil }) defer bulk.finalize() bulk.maybeSendWindowRelease(4, true) select { case <-secondAttempt: case <-time.After(time.Second): t.Fatalf("window release was not retried, calls=%d", calls.Load()) } bulk.mu.Lock() pendingBytes, pendingChunks, resetErr := bulk.pendingReleaseBytes, bulk.pendingReleaseChunks, bulk.resetErr bulk.mu.Unlock() if pendingBytes != 0 || pendingChunks != 0 { t.Fatalf("pending release after successful retry=(%d,%d), want zero", pendingBytes, pendingChunks) } if resetErr != nil { t.Fatalf("transient release timeout reset bulk: %v", resetErr) } } func TestBulkWindowReleasePermanentErrorResetsBulk(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() bulk := newBulkHandle(ctx, newBulkRuntime("buffer-release-reset-error"), clientFileScope(), BulkOpenRequest{ BulkID: "buffer-release-reset-error", DataID: 1, ChunkSize: 4, WindowBytes: 4, MaxInFlight: 1, }, 0, nil, nil, 0, nil, nil, nil, nil, func(_ *bulkHandle, bytes int64, chunks int) error { if bytes != 4 || chunks != 1 { t.Fatalf("release = (%d,%d), want (4,1)", bytes, chunks) } return errors.New("permanent release failure") }) bulk.maybeSendWindowRelease(4, true) select { case <-bulk.releaseWorkerDone: case <-time.After(time.Second): t.Fatal("window release worker did not stop after permanent error") } if err := bulk.resetErrSnapshot(); err == nil || !strings.Contains(err.Error(), "permanent release failure") { t.Fatalf("reset error=%v, want permanent release failure", err) } } func TestLegacyBulkReleaseHonorsBulkCancellation(t *testing.T) { client := NewClient().(*ClientCommon) if err := UseModernPSKClient(client, integrationSharedSecret, integrationModernPSKOptions()); err != nil { t.Fatal(err) } stopCtx, stopFn := context.WithCancel(context.Background()) defer stopFn() pipeLeft, right := net.Pipe() left := &bulkReleaseTrackingConn{Conn: pipeLeft, started: make(chan struct{})} defer left.Close() defer right.Close() queue := stario.NewQueueCtx(stopCtx, 4, math.MaxUint32) client.setClientSessionRuntime(newClientSessionRuntime(left, stopCtx, stopFn, queue, 1)) client.markSessionStarted() bulk := newBulkHandle(context.Background(), nil, "test", BulkOpenRequest{ BulkID: "legacy-release", DataID: 1, FastPathVersion: bulkFastPathVersionV1, ChunkSize: 4, WindowBytes: 4, MaxInFlight: 1, }, 0, nil, nil, 0, nil, nil, nil, nil, clientBulkReleaseSender(client)) bulk.maybeSendWindowRelease(4, true) select { case <-left.started: case <-time.After(time.Second): t.Fatal("legacy release did not enter physical write") } bulk.finalize() select { case <-bulk.releaseWorkerDone: case <-time.After(150 * time.Millisecond): t.Fatal("legacy bulk release worker ignored bulk cancellation while its send was blocked") } }