From 9fa8453a283f43ab6200b223f8f0f4bf937389ee Mon Sep 17 00:00:00 2001 From: mrtowers Date: Thu, 16 Oct 2025 18:42:33 +0000 Subject: [PATCH 01/18] added basic client structure --- src/zentropy-client.zig | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/zentropy-client.zig diff --git a/src/zentropy-client.zig b/src/zentropy-client.zig new file mode 100644 index 0000000..7a5641a --- /dev/null +++ b/src/zentropy-client.zig @@ -0,0 +1,31 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const Config = @import("config.zig"); + +/// wrapper for connecting with Zentropy server +pub const Client = struct { + ///connects to the server with `config` parameters + pub fn connect(config: Config) !Client {} + + /// destroys connection + pub fn deinit(self: *Client) void {} + + pub fn set(self: *Client, key: []const u8, value: []const u8) !void {} + /// returns bytes read + pub fn get(self: *Client, key: []const u8, out: []u8) !?usize {} + + /// caller owns memory + pub fn getAlloc(self: *Client, gpa: Allocator, key: []const u8) !?[]u8 {} + + /// returns comptime known size string + pub fn getSized(self: *Client, key: []const u8, comptime size: comptime_int) !?[size]u8 {} + + /// checks if key exists + pub fn exists(self: *Client, key: []const u8) !bool {} + + /// deletes key, if key doesn't exists returns error.KeyNotFound + pub fn delete(self: *Client, key: []const u8) !bool {} + + /// shuts down server + pub fn shutdown(self: *Client) !void {} +}; From 973f38b6241f164684adafbbf40d346a8def31dd Mon Sep 17 00:00:00 2001 From: mrtowers Date: Fri, 17 Oct 2025 11:02:41 +0200 Subject: [PATCH 02/18] connecting client --- src/zentropy-client.zig | 65 +++++++++++++++++++++++++++++++++++------ 1 file changed, 56 insertions(+), 9 deletions(-) diff --git a/src/zentropy-client.zig b/src/zentropy-client.zig index 7a5641a..c78dba4 100644 --- a/src/zentropy-client.zig +++ b/src/zentropy-client.zig @@ -1,31 +1,78 @@ const std = @import("std"); +const net = std.net; +const mem = std.mem; +const Io = std.Io; +const Reader = Io.Reader; +const Stream = net.Stream; const Allocator = std.mem.Allocator; const Config = @import("config.zig"); /// wrapper for connecting with Zentropy server pub const Client = struct { + stream: net.Stream, + + const ConnectError = error{ + BadPingResponse, + } || + net.TcpConnectToAddressError || + Stream.WriteError || + Reader.Error || + net.IPv4ParseError; + ///connects to the server with `config` parameters - pub fn connect(config: Config) !Client {} + pub fn connect(config: Config) ConnectError!Client { + const stream = try net.tcpConnectToAddress(.{ + .in = try .parse(config.bind_address, config.port), + }); + + var reader = stream.reader(&[_]u8{}); + try stream.writeAll("PING"); + const pong = try reader.file_reader.interface.takeArray(4); + + if (!mem.eql(u8, pong, "PONG")) { + return error.BadPingResponse; + } + + return Client{ + .stream = stream, + }; + } /// destroys connection - pub fn deinit(self: *Client) void {} + pub fn deinit(self: *const Client) void { + self.stream.close(); + } - pub fn set(self: *Client, key: []const u8, value: []const u8) !void {} + pub fn set(self: *Client, key: []const u8, value: []const u8) !void { + _ = .{ self, key, value }; + } /// returns bytes read - pub fn get(self: *Client, key: []const u8, out: []u8) !?usize {} + pub fn get(self: *Client, key: []const u8, out: []u8) !?usize { + _ = .{ self, key, out }; + } /// caller owns memory - pub fn getAlloc(self: *Client, gpa: Allocator, key: []const u8) !?[]u8 {} + pub fn getAlloc(self: *Client, gpa: Allocator, key: []const u8) !?[]u8 { + _ = .{ self, gpa, key }; + } /// returns comptime known size string - pub fn getSized(self: *Client, key: []const u8, comptime size: comptime_int) !?[size]u8 {} + pub fn getSized(self: *Client, key: []const u8, comptime size: comptime_int) !?[size]u8 { + _ = .{ self, key, size }; + } /// checks if key exists - pub fn exists(self: *Client, key: []const u8) !bool {} + pub fn exists(self: *Client, key: []const u8) !bool { + _ = .{ self, key }; + } /// deletes key, if key doesn't exists returns error.KeyNotFound - pub fn delete(self: *Client, key: []const u8) !bool {} + pub fn delete(self: *Client, key: []const u8) !bool { + _ = .{ self, key }; + } /// shuts down server - pub fn shutdown(self: *Client) !void {} + pub fn shutdown(self: *Client) !void { + _ = self; + } }; From 39727873c62cb41788460679895e0c9bb5dcea9f Mon Sep 17 00:00:00 2001 From: mrtowers Date: Fri, 17 Oct 2025 17:20:38 +0000 Subject: [PATCH 03/18] client.connect, .deinit and .set --- src/zentropy-client.zig | 42 ++++++++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/src/zentropy-client.zig b/src/zentropy-client.zig index c78dba4..12532a9 100644 --- a/src/zentropy-client.zig +++ b/src/zentropy-client.zig @@ -1,6 +1,7 @@ const std = @import("std"); const net = std.net; const mem = std.mem; +const builtin = @import("builtin"); const Io = std.Io; const Reader = Io.Reader; const Stream = net.Stream; @@ -25,12 +26,20 @@ pub const Client = struct { .in = try .parse(config.bind_address, config.port), }); - var reader = stream.reader(&[_]u8{}); - try stream.writeAll("PING"); - const pong = try reader.file_reader.interface.takeArray(4); + //check for connectivity only in debug and release safe mod + switch (builtin.mode) { + .Debug, .ReleaseSafe => { + var buf: [32]u8 = undefined; + var reader = stream.reader(&buf); + try stream.writeAll("PING"); + const expected_result = "PONG\r\n"; + const pong = try reader.file_reader.interface.takeArray(expected_result.len); - if (!mem.eql(u8, pong, "PONG")) { - return error.BadPingResponse; + if (!mem.eql(u8, pong, expected_result)) { + return error.BadPingResponse; + } + }, + else => {}, } return Client{ @@ -43,8 +52,27 @@ pub const Client = struct { self.stream.close(); } - pub fn set(self: *Client, key: []const u8, value: []const u8) !void { - _ = .{ self, key, value }; + const SetError = error{ + ServerError, + } || + Io.Writer.Error || + Io.Reader.Error; + + pub fn set(self: *Client, key: []const u8, value: []const u8) SetError!void { + var buf: [4096]u8 = undefined; + var writer = self.stream.writer(&buf); + + try writer.interface.print("SET {s} {s}", .{ key, value }); + try writer.interface.flush(); + + var reader = self.stream.reader(&buf); + const expected_result = "+OK\r\n"; + const result = try reader.file_reader.interface.takeByte(); // reading only 1 byte for micro boost in performance + try reader.file_reader.interface.discardAll(expected_result.len - 1); //discarding rest of the result + + if (result != '+') { + return error.ServerError; + } } /// returns bytes read pub fn get(self: *Client, key: []const u8, out: []u8) !?usize { From c84bf46c5ae562033d6fdf40c83c940a530c6c62 Mon Sep 17 00:00:00 2001 From: mrtowers Date: Sat, 18 Oct 2025 15:47:23 +0200 Subject: [PATCH 04/18] added zentropy module to the tests --- build.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.zig b/build.zig index 44cc65d..4e00dd2 100644 --- a/build.zig +++ b/build.zig @@ -6,6 +6,7 @@ pub fn build(b: *std.Build) void { const mod = b.addModule("zentropy", .{ .root_source_file = b.path("src/root.zig"), .target = target, + .optimize = optimize, }); const exe = b.addExecutable(.{ @@ -36,6 +37,7 @@ pub fn build(b: *std.Build) void { const mod_tests = b.addTest(.{ .root_module = mod, }); + mod_tests.root_module.addImport("zentropy", mod); // A run step that will run the test executable. const run_mod_tests = b.addRunArtifact(mod_tests); From a2ceee713d8fb1c32aad167bf0300c12ea36c248 Mon Sep 17 00:00:00 2001 From: mrtowers Date: Sat, 18 Oct 2025 15:49:46 +0200 Subject: [PATCH 05/18] added .shutdown and changed .get signature --- src/zentropy-client.zig | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/zentropy-client.zig b/src/zentropy-client.zig index 12532a9..c26778c 100644 --- a/src/zentropy-client.zig +++ b/src/zentropy-client.zig @@ -31,8 +31,8 @@ pub const Client = struct { .Debug, .ReleaseSafe => { var buf: [32]u8 = undefined; var reader = stream.reader(&buf); - try stream.writeAll("PING"); - const expected_result = "PONG\r\n"; + try stream.writeAll("PING"); //TODO replace with writer, writeAll is deprecated + const expected_result = "+PONG\r\n"; const pong = try reader.file_reader.interface.takeArray(expected_result.len); if (!mem.eql(u8, pong, expected_result)) { @@ -74,9 +74,10 @@ pub const Client = struct { return error.ServerError; } } - /// returns bytes read - pub fn get(self: *Client, key: []const u8, out: []u8) !?usize { + /// returns result slice pointing in `out` + pub fn get(self: *Client, key: []const u8, out: []u8) !?[]const u8 { _ = .{ self, key, out }; + return null; } /// caller owns memory @@ -99,8 +100,23 @@ pub const Client = struct { _ = .{ self, key }; } + const ShutdownError = error{ + BadResponse, + } || Io.Writer.Error || Io.Reader.Error; + /// shuts down server - pub fn shutdown(self: *Client) !void { - _ = self; + pub fn shutdown(self: *Client) ShutdownError!void { + var buf: [32]u8 = undefined; + var writer = self.stream.writer(&buf); + try writer.interface.writeAll("SHUTDOWN"); + try writer.interface.flush(); + + const expected_result = "===SHUTDOWN===\r\n"; + + var reader = self.stream.reader(&buf); + const result = try reader.file_reader.interface.takeArray(expected_result.len); + if (!mem.eql(u8, expected_result, result)) { + return error.BadResponse; + } } }; From ebb6cb1a7fe4625aa58032940ea586ad1e9d81b8 Mon Sep 17 00:00:00 2001 From: mrtowers Date: Sat, 18 Oct 2025 15:50:02 +0200 Subject: [PATCH 06/18] added clientTest.zig --- src/root.zig | 3 +++ src/tests/clientTest.zig | 56 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 src/tests/clientTest.zig diff --git a/src/root.zig b/src/root.zig index efe0ce9..834e659 100644 --- a/src/root.zig +++ b/src/root.zig @@ -1,7 +1,10 @@ const std = @import("std"); +pub const Client = @import("zentropy-client.zig").Client; + test { _ = @import("tests/KVStoreTests.zig"); _ = @import("tests/tcpTests.zig"); _ = @import("tests/unixSocketTest.zig"); + _ = @import("tests/clientTest.zig"); } diff --git a/src/tests/clientTest.zig b/src/tests/clientTest.zig new file mode 100644 index 0000000..5811dfc --- /dev/null +++ b/src/tests/clientTest.zig @@ -0,0 +1,56 @@ +const std = @import("std"); +const zentropy = @import("zentropy"); +const testing = std.testing; +const KVStore = @import("../KVStore.zig"); +const config = @import("../config.zig"); +const tcp = @import("../tcp.zig"); +const time = std.time; +const Thread = std.Thread; + +var stop_server = std.atomic.Value(bool).init(false); + +test "connect" { + stop_server.store(false, .unordered); + const server = try Thread.spawn(.{}, startServer, .{}); + + Thread.sleep(time.ns_per_ms * 1000); + + var client = try zentropy.Client.connect(.{}); + client.shutdown() catch unreachable; + client.deinit(); + + server.join(); + Thread.sleep(time.ns_per_ms * 1000); +} + +test "set-get" { + stop_server.store(false, .unordered); + const server = try Thread.spawn(.{}, startServer, .{}); + + Thread.sleep(time.ns_per_ms * 1000); + + var client = try zentropy.Client.connect(.{}); + + try client.set("example1", "value1"); + try client.set("example2", "value2"); + + var value1_buf: [32]u8 = undefined; + var value2_buf: [32]u8 = undefined; + + const value1 = try client.get("example1", &value1_buf) orelse unreachable; + try testing.expectEqualSlices(u8, "value1", value1); + const value2 = try client.get("example2", &value2_buf) orelse unreachable; + try testing.expectEqualSlices(u8, "value2", value2); + client.shutdown() catch unreachable; + client.deinit(); + + server.join(); +} + +fn startServer() !void { + var store = KVStore.init(testing.allocator); + defer store.deinit(); + var app_config = try config.load(testing.allocator); + defer app_config.deinit(testing.allocator); + try tcp.startServer(&store, &stop_server, &app_config); +} From 13ea5d6d12f3fe226d97f54334d936f57ed0116a Mon Sep 17 00:00:00 2001 From: mrtowers Date: Sun, 19 Oct 2025 19:37:53 +0200 Subject: [PATCH 07/18] client .get --- src/zentropy-client.zig | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/zentropy-client.zig b/src/zentropy-client.zig index c26778c..02b9675 100644 --- a/src/zentropy-client.zig +++ b/src/zentropy-client.zig @@ -76,8 +76,16 @@ pub const Client = struct { } /// returns result slice pointing in `out` pub fn get(self: *Client, key: []const u8, out: []u8) !?[]const u8 { - _ = .{ self, key, out }; - return null; + var buf: [4096]u8 = undefined; + var writer = self.stream.writer(&buf); + + try writer.interface.print("GET {s}", .{key}); + try writer.interface.flush(); + + var reader = self.stream.reader(out); + _ = &reader; + //TODO + @compileError("todo"); } /// caller owns memory From 3a1e9472d8c8362526e7436270faeb8af4a2c9c0 Mon Sep 17 00:00:00 2001 From: mrtowers Date: Mon, 20 Oct 2025 12:49:13 +0200 Subject: [PATCH 08/18] client test file done --- src/tests/clientTest.zig | 40 ++++++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/src/tests/clientTest.zig b/src/tests/clientTest.zig index 5811dfc..dd98acd 100644 --- a/src/tests/clientTest.zig +++ b/src/tests/clientTest.zig @@ -29,18 +29,46 @@ test "set-get" { Thread.sleep(time.ns_per_ms * 1000); + const ex1 = "example1"; + const ex2 = "example 2"; //with spaces + const val1 = "value1"; + const val2 = "value 2"; + var client = try zentropy.Client.connect(.{}); - try client.set("example1", "value1"); - try client.set("example2", "value2"); + try client.set(ex1, val1); + try client.set(ex2, val2); var value1_buf: [32]u8 = undefined; var value2_buf: [32]u8 = undefined; + var value3_buf: [32]u8 = undefined; + + // simple .get + const value1 = try client.get(ex1, &value1_buf) orelse unreachable; + try testing.expectEqualSlices(u8, val1, value1); + const value2 = try client.get(ex2, &value2_buf) orelse unreachable; + try testing.expectEqualSlices(u8, val2, value2); + const value3 = try client.get("not existing", &value3_buf); + try testing.expect(value3 == null); + + // .getAlloc + const value1_alloc = try client.getAlloc(testing.allocator, ex1) orelse unreachable; + defer testing.allocator.free(value1_alloc); + try testing.expectEqualSlices(u8, val1, value1_alloc); + const value2_alloc = try client.getAlloc(testing.allocator, ex2) orelse unreachable; + defer testing.allocator.free(value2_alloc); + try testing.expectEqualSlices(u8, val2, value2_alloc); + const value3_alloc = try client.getAlloc(testing.allocator, "not existing"); + try testing.expect(value3_alloc == null); + + // .getSized + const value1_sized = try client.getSized(ex1, val1.len) orelse unreachable; + try testing.expectEqualSlices(u8, val1, value1_sized); + const value2_sized = try client.getSized(ex2, val2.len) orelse unreachable; + try testing.expectEqualSlices(u8, val2, value2_sized); + const value3_sized = try client.getSized("not existing", 5); + try testing.expect(value3_sized == null); - const value1 = try client.get("example1", &value1_buf) orelse unreachable; - try testing.expectEqualSlices(u8, "value1", value1); - const value2 = try client.get("example2", &value2_buf) orelse unreachable; - try testing.expectEqualSlices(u8, "value2", value2); client.shutdown() catch unreachable; client.deinit(); From f2eb0e7aaa9a2482ff4ef99b51abe2bc83c33ad2 Mon Sep 17 00:00:00 2001 From: mrtowers Date: Mon, 20 Oct 2025 13:34:20 +0200 Subject: [PATCH 09/18] .getSized working --- src/zentropy-client.zig | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/zentropy-client.zig b/src/zentropy-client.zig index 02b9675..f8e53c4 100644 --- a/src/zentropy-client.zig +++ b/src/zentropy-client.zig @@ -26,7 +26,7 @@ pub const Client = struct { .in = try .parse(config.bind_address, config.port), }); - //check for connectivity only in debug and release safe mod + //check for connectivity only in debug and release safe mode switch (builtin.mode) { .Debug, .ReleaseSafe => { var buf: [32]u8 = undefined; @@ -62,7 +62,7 @@ pub const Client = struct { var buf: [4096]u8 = undefined; var writer = self.stream.writer(&buf); - try writer.interface.print("SET {s} {s}", .{ key, value }); + try writer.interface.print("SET \"{s}\" \"{s}\"", .{ key, value }); try writer.interface.flush(); var reader = self.stream.reader(&buf); @@ -79,7 +79,7 @@ pub const Client = struct { var buf: [4096]u8 = undefined; var writer = self.stream.writer(&buf); - try writer.interface.print("GET {s}", .{key}); + try writer.interface.print("GET \"{s}\"", .{key}); try writer.interface.flush(); var reader = self.stream.reader(out); @@ -95,7 +95,25 @@ pub const Client = struct { /// returns comptime known size string pub fn getSized(self: *Client, key: []const u8, comptime size: comptime_int) !?[size]u8 { - _ = .{ self, key, size }; + var buf: [4096]u8 = undefined; + var writer = self.stream.writer(&buf); + + try writer.interface.print("GET \"{s}\"", .{key}); + try writer.interface.flush(); + + var reader = self.stream.reader(&buf); + + const peek = try reader.file_reader.interface.peek(4); + if (mem.eql(u8, peek, "NONE")) { + return null; + } + + const result = try reader.file_reader.interface.takeArray(size); + var output: [result.len]u8 = undefined; + output = result.*; + _ = try reader.file_reader.interface.discardShort(2); + + return output; } /// checks if key exists From 143f78d2ede769f30beacf11291501b5c99d4e2e Mon Sep 17 00:00:00 2001 From: mrtowers Date: Mon, 20 Oct 2025 13:46:32 +0200 Subject: [PATCH 10/18] .get working --- src/zentropy-client.zig | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/zentropy-client.zig b/src/zentropy-client.zig index f8e53c4..5877ebb 100644 --- a/src/zentropy-client.zig +++ b/src/zentropy-client.zig @@ -75,7 +75,7 @@ pub const Client = struct { } } /// returns result slice pointing in `out` - pub fn get(self: *Client, key: []const u8, out: []u8) !?[]const u8 { + pub fn get(self: *Client, key: []const u8, out: []u8) !?[]u8 { var buf: [4096]u8 = undefined; var writer = self.stream.writer(&buf); @@ -83,9 +83,16 @@ pub const Client = struct { try writer.interface.flush(); var reader = self.stream.reader(out); - _ = &reader; - //TODO - @compileError("todo"); + + const peek = try reader.file_reader.interface.peek(4); + if (mem.eql(u8, peek, "NONE")) { + try reader.file_reader.interface.discardAll(6); //TODO make it use "NONE" variable, not plain "6" + return null; + } + const slice = try reader.file_reader.interface.takeDelimiter('\r'); + try reader.file_reader.interface.discardAll(1); + + return slice; } /// caller owns memory From 253e0535b98191bdacd071a3debc8be6a508bac5 Mon Sep 17 00:00:00 2001 From: mrtowers Date: Mon, 20 Oct 2025 13:58:48 +0200 Subject: [PATCH 11/18] .getAlloc working --- src/tests/clientTest.zig | 10 +++++----- src/zentropy-client.zig | 18 +++++++++++++++++- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/tests/clientTest.zig b/src/tests/clientTest.zig index dd98acd..3d1283c 100644 --- a/src/tests/clientTest.zig +++ b/src/tests/clientTest.zig @@ -13,21 +13,21 @@ test "connect" { stop_server.store(false, .unordered); const server = try Thread.spawn(.{}, startServer, .{}); - Thread.sleep(time.ns_per_ms * 1000); + Thread.sleep(time.ns_per_ms * 200); var client = try zentropy.Client.connect(.{}); client.shutdown() catch unreachable; client.deinit(); server.join(); - Thread.sleep(time.ns_per_ms * 1000); + Thread.sleep(time.ns_per_ms * 200); } test "set-get" { stop_server.store(false, .unordered); const server = try Thread.spawn(.{}, startServer, .{}); - Thread.sleep(time.ns_per_ms * 1000); + Thread.sleep(time.ns_per_ms * 200); const ex1 = "example1"; const ex2 = "example 2"; //with spaces @@ -63,9 +63,9 @@ test "set-get" { // .getSized const value1_sized = try client.getSized(ex1, val1.len) orelse unreachable; - try testing.expectEqualSlices(u8, val1, value1_sized); + try testing.expectEqualSlices(u8, val1, &value1_sized); const value2_sized = try client.getSized(ex2, val2.len) orelse unreachable; - try testing.expectEqualSlices(u8, val2, value2_sized); + try testing.expectEqualSlices(u8, val2, &value2_sized); const value3_sized = try client.getSized("not existing", 5); try testing.expect(value3_sized == null); diff --git a/src/zentropy-client.zig b/src/zentropy-client.zig index 5877ebb..0e4419b 100644 --- a/src/zentropy-client.zig +++ b/src/zentropy-client.zig @@ -97,7 +97,22 @@ pub const Client = struct { /// caller owns memory pub fn getAlloc(self: *Client, gpa: Allocator, key: []const u8) !?[]u8 { - _ = .{ self, gpa, key }; + var buf: [4096]u8 = undefined; + var writer = self.stream.writer(&buf); + + try writer.interface.print("GET \"{s}\"", .{key}); + try writer.interface.flush(); + + var reader = self.stream.reader(&buf); + const peek = try reader.file_reader.interface.peek(4); + if (mem.eql(u8, peek, "NONE")) { + try reader.file_reader.interface.discardAll(6); //TODO make it use "NONE" variable, not plain "6" + return null; + } + const slice = try reader.file_reader.interface.takeDelimiter('\r') orelse unreachable; + try reader.file_reader.interface.discardAll(1); + + return try gpa.dupe(u8, slice); } /// returns comptime known size string @@ -112,6 +127,7 @@ pub const Client = struct { const peek = try reader.file_reader.interface.peek(4); if (mem.eql(u8, peek, "NONE")) { + try reader.file_reader.interface.discardAll(6); //TODO make it use "NONE" variable, not plain "6" return null; } From 8a0e8d4ee01dd33a1091e984a13af656ad2dbaa1 Mon Sep 17 00:00:00 2001 From: mrtowers Date: Mon, 20 Oct 2025 18:45:19 +0200 Subject: [PATCH 12/18] .exists working --- src/tests/clientTest.zig | 28 +++++++++++++++++++++++++--- src/zentropy-client.zig | 12 +++++++++++- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/tests/clientTest.zig b/src/tests/clientTest.zig index 3d1283c..6ad1d40 100644 --- a/src/tests/clientTest.zig +++ b/src/tests/clientTest.zig @@ -26,6 +26,7 @@ test "connect" { test "set-get" { stop_server.store(false, .unordered); const server = try Thread.spawn(.{}, startServer, .{}); + defer server.join(); Thread.sleep(time.ns_per_ms * 200); @@ -35,6 +36,10 @@ test "set-get" { const val2 = "value 2"; var client = try zentropy.Client.connect(.{}); + defer { + client.shutdown() catch unreachable; + client.deinit(); + } try client.set(ex1, val1); try client.set(ex2, val2); @@ -68,11 +73,28 @@ test "set-get" { try testing.expectEqualSlices(u8, val2, &value2_sized); const value3_sized = try client.getSized("not existing", 5); try testing.expect(value3_sized == null); +} - client.shutdown() catch unreachable; - client.deinit(); +test "exists" { + stop_server.store(false, .unordered); + const server = try Thread.spawn(.{}, startServer, .{}); + defer server.join(); - server.join(); + Thread.sleep(time.ns_per_ms * 200); + + var client = try zentropy.Client.connect(.{}); + defer { + client.shutdown() catch unreachable; + client.deinit(); + } + + try testing.expect(!try client.exists("example1")); + try testing.expect(!try client.exists("example2")); + try client.set("example1", "value1"); + try testing.expect(try client.exists("example1")); + try testing.expect(!try client.exists("example2")); //double check + try client.set("example2", "value2"); + try testing.expect(try client.exists("example2")); } fn startServer() !void { diff --git a/src/zentropy-client.zig b/src/zentropy-client.zig index 0e4419b..6eaac27 100644 --- a/src/zentropy-client.zig +++ b/src/zentropy-client.zig @@ -141,7 +141,17 @@ pub const Client = struct { /// checks if key exists pub fn exists(self: *Client, key: []const u8) !bool { - _ = .{ self, key }; + var buf: [4096]u8 = undefined; + var writer = self.stream.writer(&buf); + + try writer.interface.print("EXISTS \"{s}\"", .{key}); + try writer.interface.flush(); + + var reader = self.stream.reader(&buf); + const exists_byte = try reader.file_reader.interface.takeByte(); + try reader.file_reader.interface.discardAll(2); + + return if (exists_byte == '1') true else false; } /// deletes key, if key doesn't exists returns error.KeyNotFound From 14d3f639565e3efcc87a241599437e3aee4e768f Mon Sep 17 00:00:00 2001 From: mrtowers Date: Mon, 20 Oct 2025 19:17:59 +0200 Subject: [PATCH 13/18] .delete working and added responses struct --- src/tests/clientTest.zig | 26 +++++++++++++++++++ src/zentropy-client.zig | 54 +++++++++++++++++++++++++++------------- 2 files changed, 63 insertions(+), 17 deletions(-) diff --git a/src/tests/clientTest.zig b/src/tests/clientTest.zig index 6ad1d40..3ea72a0 100644 --- a/src/tests/clientTest.zig +++ b/src/tests/clientTest.zig @@ -97,6 +97,32 @@ test "exists" { try testing.expect(try client.exists("example2")); } +test "delete" { + stop_server.store(false, .unordered); + const server = try Thread.spawn(.{}, startServer, .{}); + defer server.join(); + + Thread.sleep(time.ns_per_ms * 200); + + var client = try zentropy.Client.connect(.{}); + defer { + client.shutdown() catch unreachable; + client.deinit(); + } + + try testing.expect(!try client.delete("example1")); + try testing.expect(!try client.delete("example2")); + try client.set("example1", "value"); + try testing.expect(!try client.delete("example2")); + const val1 = try client.getAlloc(testing.allocator, "example1") orelse unreachable; + defer testing.allocator.free(val1); + try testing.expectEqualSlices(u8, "value", val1); + try testing.expect(try client.delete("example1")); + try testing.expect(!try client.delete("example1")); + const val2 = try client.getAlloc(testing.allocator, "example1"); + try testing.expect(val2 == null); +} + fn startServer() !void { var store = KVStore.init(testing.allocator); defer store.deinit(); diff --git a/src/zentropy-client.zig b/src/zentropy-client.zig index 6eaac27..d8bb893 100644 --- a/src/zentropy-client.zig +++ b/src/zentropy-client.zig @@ -66,9 +66,8 @@ pub const Client = struct { try writer.interface.flush(); var reader = self.stream.reader(&buf); - const expected_result = "+OK\r\n"; const result = try reader.file_reader.interface.takeByte(); // reading only 1 byte for micro boost in performance - try reader.file_reader.interface.discardAll(expected_result.len - 1); //discarding rest of the result + try reader.file_reader.interface.discardAll(responses.ok.len - 1); //discarding rest of the result if (result != '+') { return error.ServerError; @@ -84,13 +83,13 @@ pub const Client = struct { var reader = self.stream.reader(out); - const peek = try reader.file_reader.interface.peek(4); - if (mem.eql(u8, peek, "NONE")) { - try reader.file_reader.interface.discardAll(6); //TODO make it use "NONE" variable, not plain "6" + const peek = try reader.file_reader.interface.peek(responses.none.len); + if (mem.eql(u8, peek, responses.none)) { + try reader.file_reader.interface.discardAll(responses.none.len); return null; } const slice = try reader.file_reader.interface.takeDelimiter('\r'); - try reader.file_reader.interface.discardAll(1); + try reader.file_reader.interface.discardAll(1); //discard "\n" return slice; } @@ -104,13 +103,13 @@ pub const Client = struct { try writer.interface.flush(); var reader = self.stream.reader(&buf); - const peek = try reader.file_reader.interface.peek(4); - if (mem.eql(u8, peek, "NONE")) { - try reader.file_reader.interface.discardAll(6); //TODO make it use "NONE" variable, not plain "6" + const peek = try reader.file_reader.interface.peek(responses.none.len); + if (mem.eql(u8, peek, responses.none)) { + try reader.file_reader.interface.discardAll(responses.none.len); return null; } const slice = try reader.file_reader.interface.takeDelimiter('\r') orelse unreachable; - try reader.file_reader.interface.discardAll(1); + try reader.file_reader.interface.discardAll(1); //discard "\n" return try gpa.dupe(u8, slice); } @@ -125,16 +124,16 @@ pub const Client = struct { var reader = self.stream.reader(&buf); - const peek = try reader.file_reader.interface.peek(4); - if (mem.eql(u8, peek, "NONE")) { - try reader.file_reader.interface.discardAll(6); //TODO make it use "NONE" variable, not plain "6" + const peek = try reader.file_reader.interface.peek(responses.none.len); + if (mem.eql(u8, peek, responses.none)) { + try reader.file_reader.interface.discardAll(responses.none.len); return null; } const result = try reader.file_reader.interface.takeArray(size); var output: [result.len]u8 = undefined; output = result.*; - _ = try reader.file_reader.interface.discardShort(2); + _ = try reader.file_reader.interface.discardShort(2); //discard "\r\n" return output; } @@ -149,14 +148,28 @@ pub const Client = struct { var reader = self.stream.reader(&buf); const exists_byte = try reader.file_reader.interface.takeByte(); - try reader.file_reader.interface.discardAll(2); + try reader.file_reader.interface.discardAll(2); //discard "\r\n" return if (exists_byte == '1') true else false; } - /// deletes key, if key doesn't exists returns error.KeyNotFound + /// deletes key, returns true if deleted pub fn delete(self: *Client, key: []const u8) !bool { - _ = .{ self, key }; + var buf: [4096]u8 = undefined; + var writer = self.stream.writer(&buf); + + try writer.interface.print("DELETE \"{s}\"", .{key}); + try writer.interface.flush(); + + var reader = self.stream.reader(&buf); + + const peek = try reader.file_reader.interface.peek(responses.ok.len); + if (mem.eql(u8, peek, responses.ok)) { + try reader.file_reader.interface.discardAll(responses.ok.len); + return true; + } + try reader.file_reader.interface.discardAll(responses.not_deleted.len); + return false; } const ShutdownError = error{ @@ -179,3 +192,10 @@ pub const Client = struct { } } }; + +const responses = struct { + pub const shutdown = "===SHUTDOWN===\r\n"; + pub const ok = "+OK\r\n"; + pub const not_deleted = "-NOT DELETED\r\n"; + pub const none = "NONE\r\n"; +}; From 6dc180323e8448057b21de3ccfd58f1d40f0fabc Mon Sep 17 00:00:00 2001 From: mrtowers Date: Mon, 20 Oct 2025 21:12:33 +0200 Subject: [PATCH 14/18] waiting for start/stop server in test --- src/tests/clientTest.zig | 54 +++++++++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/src/tests/clientTest.zig b/src/tests/clientTest.zig index 3ea72a0..7fa0e2c 100644 --- a/src/tests/clientTest.zig +++ b/src/tests/clientTest.zig @@ -7,28 +7,25 @@ const tcp = @import("../tcp.zig"); const time = std.time; const Thread = std.Thread; -var stop_server = std.atomic.Value(bool).init(false); - test "connect" { - stop_server.store(false, .unordered); const server = try Thread.spawn(.{}, startServer, .{}); + defer server.join(); - Thread.sleep(time.ns_per_ms * 200); + try waitForServerStarted(); var client = try zentropy.Client.connect(.{}); - client.shutdown() catch unreachable; - client.deinit(); - - server.join(); - Thread.sleep(time.ns_per_ms * 200); + defer { + client.shutdown() catch unreachable; + client.deinit(); + waitForServerStopped() catch unreachable; + } } test "set-get" { - stop_server.store(false, .unordered); const server = try Thread.spawn(.{}, startServer, .{}); defer server.join(); - Thread.sleep(time.ns_per_ms * 200); + try waitForServerStarted(); const ex1 = "example1"; const ex2 = "example 2"; //with spaces @@ -39,6 +36,7 @@ test "set-get" { defer { client.shutdown() catch unreachable; client.deinit(); + waitForServerStopped() catch unreachable; } try client.set(ex1, val1); @@ -76,16 +74,16 @@ test "set-get" { } test "exists" { - stop_server.store(false, .unordered); const server = try Thread.spawn(.{}, startServer, .{}); defer server.join(); - Thread.sleep(time.ns_per_ms * 200); + try waitForServerStarted(); var client = try zentropy.Client.connect(.{}); defer { client.shutdown() catch unreachable; client.deinit(); + waitForServerStopped() catch unreachable; } try testing.expect(!try client.exists("example1")); @@ -98,16 +96,16 @@ test "exists" { } test "delete" { - stop_server.store(false, .unordered); const server = try Thread.spawn(.{}, startServer, .{}); defer server.join(); - Thread.sleep(time.ns_per_ms * 200); + try waitForServerStarted(); var client = try zentropy.Client.connect(.{}); defer { client.shutdown() catch unreachable; client.deinit(); + waitForServerStopped() catch unreachable; } try testing.expect(!try client.delete("example1")); @@ -123,7 +121,33 @@ test "delete" { try testing.expect(val2 == null); } +fn waitForServerStarted() !void { + for (0..10000) |_| { + const client = zentropy.Client.connect(.{}) catch { + Thread.sleep(time.ns_per_us * 50); + continue; + }; + defer client.deinit(); + return; + } + return error.Timeout; +} + +fn waitForServerStopped() !void { + for (0..10000) |_| { + const client = zentropy.Client.connect(.{}) catch { + return; + }; + client.deinit(); + Thread.sleep(time.ns_per_us * 50); + continue; + } + + return error.Timeout; +} + fn startServer() !void { + var stop_server = std.atomic.Value(bool).init(false); var store = KVStore.init(testing.allocator); defer store.deinit(); var app_config = try config.load(testing.allocator); From 7cb9140ea569a699eccf1f9c101eb4dd0f8b4954 Mon Sep 17 00:00:00 2001 From: mrtowers Date: Mon, 20 Oct 2025 21:15:48 +0200 Subject: [PATCH 15/18] shutdown response usage --- src/zentropy-client.zig | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/zentropy-client.zig b/src/zentropy-client.zig index d8bb893..b1b2d58 100644 --- a/src/zentropy-client.zig +++ b/src/zentropy-client.zig @@ -183,11 +183,9 @@ pub const Client = struct { try writer.interface.writeAll("SHUTDOWN"); try writer.interface.flush(); - const expected_result = "===SHUTDOWN===\r\n"; - var reader = self.stream.reader(&buf); - const result = try reader.file_reader.interface.takeArray(expected_result.len); - if (!mem.eql(u8, expected_result, result)) { + const result = try reader.file_reader.interface.takeArray(responses.shutdown.len); + if (!mem.eql(u8, responses.shutdown, result)) { return error.BadResponse; } } From 8377653a0719d4164fa66698c4ab91eade46f774 Mon Sep 17 00:00:00 2001 From: mrtowers Date: Mon, 20 Oct 2025 21:18:20 +0200 Subject: [PATCH 16/18] buffer size variable --- src/zentropy-client.zig | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/zentropy-client.zig b/src/zentropy-client.zig index b1b2d58..dbc71a1 100644 --- a/src/zentropy-client.zig +++ b/src/zentropy-client.zig @@ -12,6 +12,8 @@ const Config = @import("config.zig"); pub const Client = struct { stream: net.Stream, + const buffer_size = 4096; // this affects responses max size + const ConnectError = error{ BadPingResponse, } || @@ -59,7 +61,7 @@ pub const Client = struct { Io.Reader.Error; pub fn set(self: *Client, key: []const u8, value: []const u8) SetError!void { - var buf: [4096]u8 = undefined; + var buf: [buffer_size]u8 = undefined; var writer = self.stream.writer(&buf); try writer.interface.print("SET \"{s}\" \"{s}\"", .{ key, value }); @@ -75,7 +77,7 @@ pub const Client = struct { } /// returns result slice pointing in `out` pub fn get(self: *Client, key: []const u8, out: []u8) !?[]u8 { - var buf: [4096]u8 = undefined; + var buf: [buffer_size]u8 = undefined; var writer = self.stream.writer(&buf); try writer.interface.print("GET \"{s}\"", .{key}); @@ -96,7 +98,7 @@ pub const Client = struct { /// caller owns memory pub fn getAlloc(self: *Client, gpa: Allocator, key: []const u8) !?[]u8 { - var buf: [4096]u8 = undefined; + var buf: [buffer_size]u8 = undefined; var writer = self.stream.writer(&buf); try writer.interface.print("GET \"{s}\"", .{key}); @@ -116,7 +118,7 @@ pub const Client = struct { /// returns comptime known size string pub fn getSized(self: *Client, key: []const u8, comptime size: comptime_int) !?[size]u8 { - var buf: [4096]u8 = undefined; + var buf: [buffer_size]u8 = undefined; var writer = self.stream.writer(&buf); try writer.interface.print("GET \"{s}\"", .{key}); @@ -140,7 +142,7 @@ pub const Client = struct { /// checks if key exists pub fn exists(self: *Client, key: []const u8) !bool { - var buf: [4096]u8 = undefined; + var buf: [buffer_size]u8 = undefined; var writer = self.stream.writer(&buf); try writer.interface.print("EXISTS \"{s}\"", .{key}); @@ -155,7 +157,7 @@ pub const Client = struct { /// deletes key, returns true if deleted pub fn delete(self: *Client, key: []const u8) !bool { - var buf: [4096]u8 = undefined; + var buf: [buffer_size]u8 = undefined; var writer = self.stream.writer(&buf); try writer.interface.print("DELETE \"{s}\"", .{key}); From 5739104b8415da3a0f0ec87c7df05fa91d1ed48d Mon Sep 17 00:00:00 2001 From: mrtowers Date: Mon, 20 Oct 2025 21:20:41 +0200 Subject: [PATCH 17/18] discarded all unreachable --- src/zentropy-client.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zentropy-client.zig b/src/zentropy-client.zig index dbc71a1..5d224e4 100644 --- a/src/zentropy-client.zig +++ b/src/zentropy-client.zig @@ -110,7 +110,7 @@ pub const Client = struct { try reader.file_reader.interface.discardAll(responses.none.len); return null; } - const slice = try reader.file_reader.interface.takeDelimiter('\r') orelse unreachable; + const slice = try reader.file_reader.interface.takeDelimiter('\r') orelse return null; try reader.file_reader.interface.discardAll(1); //discard "\n" return try gpa.dupe(u8, slice); From 6868673d2f3180a630d4a1eda00487c63dc8ab7e Mon Sep 17 00:00:00 2001 From: mrtowers Date: Mon, 20 Oct 2025 21:30:33 +0200 Subject: [PATCH 18/18] resolved todos --- src/zentropy-client.zig | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/zentropy-client.zig b/src/zentropy-client.zig index 5d224e4..17d97fd 100644 --- a/src/zentropy-client.zig +++ b/src/zentropy-client.zig @@ -18,7 +18,7 @@ pub const Client = struct { BadPingResponse, } || net.TcpConnectToAddressError || - Stream.WriteError || + Io.Writer.Error || Reader.Error || net.IPv4ParseError; @@ -32,12 +32,13 @@ pub const Client = struct { switch (builtin.mode) { .Debug, .ReleaseSafe => { var buf: [32]u8 = undefined; + var writer = stream.writer(&buf); + try writer.interface.writeAll("PING"); + try writer.interface.flush(); var reader = stream.reader(&buf); - try stream.writeAll("PING"); //TODO replace with writer, writeAll is deprecated - const expected_result = "+PONG\r\n"; - const pong = try reader.file_reader.interface.takeArray(expected_result.len); + const pong = try reader.file_reader.interface.takeArray(responses.pong.len); - if (!mem.eql(u8, pong, expected_result)) { + if (!mem.eql(u8, pong, responses.pong)) { return error.BadPingResponse; } }, @@ -198,4 +199,5 @@ const responses = struct { pub const ok = "+OK\r\n"; pub const not_deleted = "-NOT DELETED\r\n"; pub const none = "NONE\r\n"; + pub const pong = "+PONG\r\n"; };