From 2b2b6b1ecdbac5d86079d0aabcf127bd690055e1 Mon Sep 17 00:00:00 2001 From: zzstoatzz Date: Tue, 8 Sep 2026 20:52:39 -0500 Subject: [PATCH] fix: enable live compression with dictionary rotation recovery --- docs/client.md | 5 +++ docs/compression-review.md | 51 ++++++++++++++++++++++ src/client.zig | 19 +++++++- src/loopback_test.zig | 86 +++++++++++++++++++++++++++++++++++-- src/testdata/README.md | 7 +++ src/testdata/live.dict | Bin 0 -> 65536 bytes 6 files changed, 163 insertions(+), 5 deletions(-) create mode 100644 docs/compression-review.md create mode 100644 src/testdata/README.md create mode 100644 src/testdata/live.dict diff --git a/docs/client.md b/docs/client.md index 862f93a..9f15c63 100644 --- a/docs/client.md +++ b/docs/client.md @@ -93,3 +93,8 @@ positive server delay in place of the fallback; whole-segment requests use no jitter and the longer of the fallback and server delay. Absolute `RateLimit-Reset` takes precedence over `Retry-After` (seconds or IMF-fixdate). Past hints use the fallback, and all server-directed waits are capped at 30s. + +Live dictionary-zstd compression is enabled by default. Set +`zstd_compression = false` to disable dictionary fetching and negotiation. +A rejected dictionary is refetched from the same live host; a failed, invalid, +or unchanged refresh falls back to uncompressed delivery. diff --git a/docs/compression-review.md b/docs/compression-review.md new file mode 100644 index 0000000..f91c1a2 --- /dev/null +++ b/docs/compression-review.md @@ -0,0 +1,51 @@ +# Compression verification + +The unified client enables dictionary compression by default and supplies the +refresh callback to both live-only and archive-cutover consumers. Default +negotiation and refresh tests failed before their respective fixes. The 57-test +client suite passes, including replacement dictionary, unchanged dictionary, +invalid/unavailable dictionary, and explicit disable. A ten-event live failover +smoke against stream.waow.tech passes with this candidate. + +The archive server's rejection previously used plain text. A real local +WebSocket upgrade regression fails on the old content type and passes with +HTTP 400 and the UnknownZstdDictionary JSON envelope. Its local build passes. +The server fix is commit `d0091f9`. All 21 full admission suites passed on +2026-09-06. It was deployed on 2026-09-08 using the checksummed Docker-save +transfer; its admitted config digest is +`sha256:2841d664a9b5639a6f8430b777e1d280fe2bf301a4dbb3d2bf61022cc81dee68`. +The destination Docker reports the enclosing OCI manifest digest +`sha256:c331e7b502c95f3a07af3b25101ce01b2e4a87ef9c2d5c3c5c3cc6aa608dddd4`; +the manifest, config, and every layer were hash-verified before restart. + +The performance workload and raw results belong to atproto-bench under +jetstream-compression. The 20-pair quiet baseline uses a synthetic corpus and +must not be presented as a production compression ratio. No allocation reuse +optimization has been made. + +The existing decoder still requires a declared frame content size. This +review verifies default negotiation and dictionary recovery, not support for +every valid zstd frame encoding. Unknown-content-size frames remain a separate +decoder conformance item. + +## Production verification, 2026-09-08 + +Production reports build `d0091f9`. A genuine HTTP/1.1 WebSocket upgrade +with dictionary ID 1 returns HTTP 400, application/json, and +`UnknownZstdDictionary`, naming current dictionary 20260811. The candidate +client received ten consecutive events after live host failover with default +compression enabled (24123007708..24123007717, 2375 ms). Rotation and +same-dictionary fallback are exercised by loopback tests; the production +probe did not rotate the server dictionary. + +The client suite passes 57/57 tests; examples build and formatting passes. +`zig zen` review: dictionary ownership and cleanup remain explicit, refresh +uses the selected live host, and this change adds no per-event allocation. +The existing decompressed event allocation is unchanged. + +Post-deploy cadence checks each ran 60 seconds: live attach delivered 79,148 +messages; ten-second rewind delivered 50,624. Both had one connection, zero +disconnects/errors, and increasing message counts in every interval (the +reporter prints 0/s for its initial baseline). Upstream sequence advanced +from 33,455,790,952 to 33,455,832,751. Observed RSS was 4.19 GB; drop and +compaction-error counters were zero. diff --git a/src/client.zig b/src/client.zig index afcb3c4..80d9bd5 100644 --- a/src/client.zig +++ b/src/client.zig @@ -101,10 +101,10 @@ pub const Options = struct { api_key: ?[]const u8 = null, /// block fetches in flight during the archive sweep (see ArchiveBackfill) concurrency: usize = 4, - /// opt into dict-zstd live frames: the current dictionary is fetched + /// Dict-zstd live frames are enabled by default: the current dictionary is fetched /// via getZstdDictionary; a fetch failure degrades to an uncompressed /// tail (compression is an optimization, never a stream failure) - zstd_compression: bool = false, + zstd_compression: bool = true, /// live reconnect backoff overrides (tests); zero = defaults backoff_min_ns: u64 = 0, backoff_max_ns: u64 = 0, @@ -226,12 +226,15 @@ fn runOnHost( defer if (dict_buf) |owned| allocator.free(owned); if (options.zstd_compression) dict_buf = fetchZstdDict(io, allocator, host); + var dict_source = DictSource{ .io = io, .host = host }; const H = @TypeOf(handler.*); if (after_seq == null) { // pure live tail (upstream runLiveOnly) var client = live_mod.LiveClient.init(io, allocator, liveOptions(options, host, live_cursor, 0, dict_buf)); defer client.deinit(); + client.options.refetch_dict = DictSource.fetch; + client.options.refetch_dict_ctx = &dict_source; var filtering = FilteringHandler(H){ .inner = handler, .kinds = options.kinds, @@ -273,6 +276,8 @@ fn runOnHost( const cutover = @max(result.planned_through_seq, cursor); var client = live_mod.LiveClient.init(io, allocator, liveOptions(options, host, cutover, cutover, dict_buf)); defer client.deinit(); + client.options.refetch_dict = DictSource.fetch; + client.options.refetch_dict_ctx = &dict_source; var filtering = FilteringHandler(H){ .inner = handler, .kinds = options.kinds, @@ -403,6 +408,16 @@ fn wantsKind(kinds: []const livedecode.Kind, event: livedecode.Event) bool { return false; } +const DictSource = struct { + io: Io, + host: []const u8, + + fn fetch(ctx: ?*anyopaque, allocator: Allocator) ?[]u8 { + const self: *DictSource = @ptrCast(@alignCast(ctx.?)); + return fetchZstdDict(self.io, allocator, self.host); + } +}; + /// fetch the server's current live-tail compression dictionary. nil on any /// failure: compression is an optimization, so a failed fetch degrades to /// an uncompressed tail (logged) rather than failing the stream. diff --git a/src/loopback_test.zig b/src/loopback_test.zig index c381dd2..372c5c4 100644 --- a/src/loopback_test.zig +++ b/src/loopback_test.zig @@ -54,6 +54,9 @@ const Instance = struct { first_tip: ?u64 = null, stalled_plan: bool = false, archive_fault: enum { none, fetch, decode, segment } = .none, + compression: enum { none, valid, rotate, same, invalid } = .none, + dict_calls: usize = 0, + compressed_sessions: usize = 0, reject_plan: bool = false, retry_block_calls: ?usize = null, live_authorized: bool = false, @@ -141,6 +144,23 @@ const Instance = struct { try self.handleWebsocket(request, target, r, w); return false; } + if (std.mem.indexOf(u8, target, "getZstdDictionary") != null) { + self.dict_calls += 1; + if (self.compression == .none) { + try w.writeAll("HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"); + try w.flush(); + } else if (self.compression == .invalid) { + try respondBytes(w, "application/octet-stream", "invalid dictionary"); + } else { + var dict = @embedFile("testdata/live.dict").*; + if (self.compression == .rotate and self.dict_calls > 1) { + const old = std.mem.readInt(u32, dict[4..8], .little); + std.mem.writeInt(u32, dict[4..8], old + 1, .little); + } + try respondBytes(w, "application/octet-stream", &dict); + } + return false; + } const authorized = if (headerValue(request, "Authorization")) |value| std.mem.eql(u8, value, "Bearer test-archive-key") else false; self.archive_authorized = self.archive_authorized or authorized; self.archive_missing_auth = self.archive_missing_auth or !authorized; @@ -278,6 +298,14 @@ const Instance = struct { self.ws_sessions += 1; if (self.ws_sessions > self.max_ws_sessions) return; // down: slam the door + const compressed = std.mem.indexOf(u8, target, "zstdDictionary=") != null; + if (compressed) self.compressed_sessions += 1; + if (compressed and self.ws_sessions == 1 and (self.compression == .rotate or self.compression == .same)) { + const body = "{\"error\":\"UnknownZstdDictionary\"}"; + try w.print("HTTP/1.1 400 Bad Request\r\nContent-Type: application/json\r\nContent-Length: {d}\r\nConnection: close\r\n\r\n{s}", .{ body.len, body }); + try w.flush(); + return; + } const key = headerValue(request, "Sec-WebSocket-Key") orelse return error.NoKey; var sha = std.crypto.hash.Sha1.init(.{}); sha.update(key); @@ -297,7 +325,17 @@ const Instance = struct { const frame = try std.fmt.bufPrint(&frame_buf, \\{{"$type":"message","payload":{{"$type":"network.bsky.jetstream.subscribeEvents#commit","collection":"{s}","did":"{s}","operation":"create","record":{{"$type":"app.bsky.feed.post","text":"hi"}},"rev":"rev1","rkey":"rkey1","seq":{d},"time":"2026-07-13T00:00:{d:0>2}.000000Z"}}}} , .{ row.collection, row.did, row.seq, @as(u64, @intCast(row.t)) }); - try writeTextFrame(w, frame); + if (compressed) { + var buffer: [600]u8 = undefined; + var output: Io.Writer = .fixed(&buffer); + try output.writeAll(&.{ 0x28, 0xb5, 0x2f, 0xfd, 0xa3 }); + const id = std.mem.readInt(u32, @embedFile("testdata/live.dict")[4..8], .little) + @as(u32, if (self.compression == .rotate and self.dict_calls > 1) 1 else 0); + try output.writeInt(u32, id, .little); + try output.writeInt(u32, @intCast(frame.len), .little); + try output.writeInt(u24, @intCast((frame.len << 3) | 1), .little); + try output.writeAll(frame); + try writeFrame(w, output.buffered(), 0x82); + } else try writeTextFrame(w, frame); } if (self.live_then_close) return; } @@ -312,10 +350,14 @@ const Instance = struct { }; fn writeTextFrame(w: *Io.Writer, payload: []const u8) !void { + return writeFrame(w, payload, 0x81); +} + +fn writeFrame(w: *Io.Writer, payload: []const u8, opcode: u8) !void { if (payload.len < 126) { - try w.writeAll(&.{ 0x81, @as(u8, @intCast(payload.len)) }); + try w.writeAll(&.{ opcode, @as(u8, @intCast(payload.len)) }); } else { - try w.writeAll(&.{ 0x81, 126 }); + try w.writeAll(&.{ opcode, 126 }); try w.writeInt(u16, @intCast(payload.len), .big); } try w.writeAll(payload); @@ -762,3 +804,41 @@ test "archive block retries stop after three HTTP attempts" { try testing.expectEqual(@as(?usize, 3), host.retry_block_calls); try testing.expectEqual(@as(usize, 0), handler.len); } + +test "default compression negotiates and recovers dictionary rotation" { + inline for (.{ .valid, .rotate, .same, .invalid, .none }) |mode| { + var threaded: Io.Threaded = .init(testing.allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var host = try Instance.init(io, testing.allocator, &.{}, &.{.{ .seq = 1, .t = 1 }}); + host.compression = mode; + var future = try io.concurrent(Instance.serve, .{&host}); + defer host.deinit(); + defer _ = future.cancel(io) catch {}; + defer host.quit(); + var url: [64]u8 = undefined; + var handler = CollectingHandler(1){}; + try client_mod.subscribe(io, testing.allocator, .{ .hosts = &.{host.hostUrl(&url)} }, &handler); + try testing.expectEqual(@as(usize, 1), handler.len); + try testing.expectEqual(@as(usize, if (mode == .rotate or mode == .same) 2 else 1), host.dict_calls); + try testing.expectEqual(@as(usize, if (mode == .rotate) 2 else if (mode == .valid or mode == .same) 1 else 0), host.compressed_sessions); + } +} + +test "explicitly disabled compression makes no dictionary request" { + var threaded: Io.Threaded = .init(testing.allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var host = try Instance.init(io, testing.allocator, &.{}, &.{.{ .seq = 1, .t = 1 }}); + host.compression = .valid; + var future = try io.concurrent(Instance.serve, .{&host}); + defer host.deinit(); + defer _ = future.cancel(io) catch {}; + defer host.quit(); + var url: [64]u8 = undefined; + var handler = CollectingHandler(1){}; + try client_mod.subscribe(io, testing.allocator, .{ .hosts = &.{host.hostUrl(&url)}, .zstd_compression = false }, &handler); + try testing.expectEqual(@as(usize, 1), handler.len); + try testing.expectEqual(@as(usize, 0), host.dict_calls); + try testing.expectEqual(@as(usize, 0), host.compressed_sessions); +} diff --git a/src/testdata/README.md b/src/testdata/README.md new file mode 100644 index 0000000..244b481 --- /dev/null +++ b/src/testdata/README.md @@ -0,0 +1,7 @@ +# Live compression fixture + +`live.dict` is the 65,536-byte decoded v2 dictionary from the sibling archive +server's `src/assets/zstd_dictionary_v2.b64` (server base revision 1765e48). +The loopback rotation test changes only the dictionary ID in a copy to model +replacement. It emits independently framed zstd raw blocks with that ID; +actual compressed blocks are exercised by atproto-bench's generated corpus. diff --git a/src/testdata/live.dict b/src/testdata/live.dict new file mode 100644 index 0000000000000000000000000000000000000000..1f88712815a3a2abe87555adaae535f2fd117f73 GIT binary patch literal 65536 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�jN;B*gVw6DhaUQ7( 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