2
0

storage.zig 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. const std = @import("std");
  2. const index = @import("index.zig");
  3. const hashing = @import("hashing.zig");
  4. const persistence = @import("persistence.zig");
  5. const NUM_SHARDS = 64;
  6. const TOTAL_BUCKETS = 1_048_576;
  7. const BUCKETS_PER_SHARD = TOTAL_BUCKETS / NUM_SHARDS;
  8. const Entry = struct {
  9. key: []const u8,
  10. value: []const u8,
  11. hash: u32,
  12. next: ?*Entry,
  13. };
  14. const Shard = struct {
  15. buckets: []?*Entry,
  16. rwlock: std.Thread.RwLock,
  17. arena: std.heap.ArenaAllocator,
  18. allocator: std.mem.Allocator,
  19. };
  20. var shards: [NUM_SHARDS]Shard = undefined;
  21. var shards_initialized: bool = false;
  22. var init_mutex: std.Thread.Mutex = .{};
  23. fn getShardIndex(hash: u32) usize {
  24. return hash % NUM_SHARDS;
  25. }
  26. pub fn init() void {
  27. if (shards_initialized) return;
  28. init_mutex.lock();
  29. defer init_mutex.unlock();
  30. if (!shards_initialized) {
  31. for (&shards) |*shard| {
  32. shard.arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
  33. shard.allocator = shard.arena.allocator();
  34. shard.buckets = shard.allocator.alloc(?*Entry, BUCKETS_PER_SHARD) catch unreachable;
  35. @memset(shard.buckets, null);
  36. shard.rwlock = .{};
  37. }
  38. shards_initialized = true;
  39. }
  40. }
  41. pub fn restore(key: []const u8, value: []const u8) bool {
  42. const hash = hashing.hashKey(key);
  43. const shard_idx = getShardIndex(hash);
  44. var entry_key: []const u8 = undefined;
  45. {
  46. shards[shard_idx].rwlock.lock();
  47. defer shards[shard_idx].rwlock.unlock();
  48. const entry = writeVolatile(hash, key, value) orelse return false;
  49. entry_key = entry.key;
  50. }
  51. index.insert(entry_key);
  52. return true;
  53. }
  54. pub fn restoreDelete(key: []const u8) bool {
  55. const hash = hashing.hashKey(key);
  56. const shard_idx = getShardIndex(hash);
  57. const deleted = blk: {
  58. shards[shard_idx].rwlock.lock();
  59. defer shards[shard_idx].rwlock.unlock();
  60. break :blk deleteVolatile(hash, key);
  61. };
  62. if (deleted) {
  63. index.delete(key);
  64. return true;
  65. }
  66. return false;
  67. }
  68. pub fn writeVolatile(hash: u32, key: []const u8, value: []const u8) ?*Entry {
  69. const shard_idx = getShardIndex(hash);
  70. const bucketIdx = hash % shards[shard_idx].buckets.len;
  71. const alloc = shards[shard_idx].allocator;
  72. var current = shards[shard_idx].buckets[bucketIdx];
  73. while (current) |entry| {
  74. if (entry.hash == hash and std.mem.eql(u8, entry.key, key)) {
  75. alloc.free(entry.value);
  76. entry.value = alloc.dupe(u8, value) catch return null;
  77. return entry;
  78. }
  79. current = entry.next;
  80. }
  81. const newEntry = alloc.create(Entry) catch return null;
  82. errdefer alloc.destroy(newEntry);
  83. newEntry.* = Entry{
  84. .key = alloc.dupe(u8, key) catch return null,
  85. .value = alloc.dupe(u8, value) catch return null,
  86. .hash = hash, // Cache hash value
  87. .next = shards[shard_idx].buckets[bucketIdx],
  88. };
  89. shards[shard_idx].buckets[bucketIdx] = newEntry;
  90. return newEntry;
  91. }
  92. pub fn write(key: []const u8, value: []const u8) bool {
  93. const hash = hashing.hashKey(key);
  94. const shard_idx = getShardIndex(hash);
  95. {
  96. shards[shard_idx].rwlock.lock();
  97. defer shards[shard_idx].rwlock.unlock();
  98. const entry = writeVolatile(hash, key, value) orelse return false;
  99. // Prefix scans must see the index entry before this write becomes
  100. // visible to another connection.
  101. index.insert(entry.key);
  102. persistence.persist('W', entry.key, entry.value);
  103. }
  104. return true;
  105. }
  106. pub fn read(key: []const u8) ?[]const u8 {
  107. if (!shards_initialized) return null;
  108. const hash = hashing.hashKey(key);
  109. const shard_idx = getShardIndex(hash);
  110. shards[shard_idx].rwlock.lockShared();
  111. defer shards[shard_idx].rwlock.unlockShared();
  112. var current = shards[shard_idx].buckets[hash % shards[shard_idx].buckets.len];
  113. while (current) |entry| {
  114. if (entry.hash == hash and std.mem.eql(u8, entry.key, key)) {
  115. return entry.value;
  116. }
  117. current = entry.next;
  118. }
  119. return null;
  120. }
  121. pub fn readAlloc(key: []const u8, allocator: std.mem.Allocator) ?[]const u8 {
  122. if (!shards_initialized) return null;
  123. const hash = hashing.hashKey(key);
  124. const shard_idx = getShardIndex(hash);
  125. shards[shard_idx].rwlock.lockShared();
  126. defer shards[shard_idx].rwlock.unlockShared();
  127. var current = shards[shard_idx].buckets[hash % shards[shard_idx].buckets.len];
  128. while (current) |entry| {
  129. if (entry.hash == hash and std.mem.eql(u8, entry.key, key)) {
  130. return allocator.dupe(u8, entry.value) catch null;
  131. }
  132. current = entry.next;
  133. }
  134. return null;
  135. }
  136. pub fn deleteVolatile(hash: u32, key: []const u8) bool {
  137. if (!shards_initialized) return false;
  138. const shard_idx = getShardIndex(hash);
  139. const bucketIdx = hash % shards[shard_idx].buckets.len;
  140. const alloc = shards[shard_idx].allocator;
  141. var current = shards[shard_idx].buckets[bucketIdx];
  142. var prev: ?*Entry = null;
  143. while (current) |entry| {
  144. if (entry.hash == hash and std.mem.eql(u8, entry.key, key)) {
  145. if (prev) |p| {
  146. p.next = entry.next;
  147. } else {
  148. shards[shard_idx].buckets[bucketIdx] = entry.next;
  149. }
  150. alloc.free(entry.key);
  151. alloc.free(entry.value);
  152. alloc.destroy(entry);
  153. return true;
  154. }
  155. prev = entry;
  156. current = entry.next;
  157. }
  158. return false;
  159. }
  160. pub fn delete(key: []const u8) bool {
  161. const hash = hashing.hashKey(key);
  162. const shard_idx = getShardIndex(hash);
  163. const deleted = blk: {
  164. shards[shard_idx].rwlock.lock();
  165. defer shards[shard_idx].rwlock.unlock();
  166. break :blk deleteVolatile(hash, key);
  167. };
  168. if (deleted) {
  169. index.delete(key);
  170. persistence.persist('D', key, "");
  171. }
  172. return deleted;
  173. }
  174. // -- Tests --
  175. test "writeVolatile and read basic" {
  176. init();
  177. const hash = hashing.hashKey("test_key");
  178. const shard_idx = getShardIndex(hash);
  179. shards[shard_idx].rwlock.lock();
  180. _ = writeVolatile(hash, "test_key", "test_value");
  181. shards[shard_idx].rwlock.unlock();
  182. const val = read("test_key") orelse return error.TestUnexpectedResult;
  183. try std.testing.expectEqualStrings("test_value", val);
  184. }
  185. test "writeVolatile overwrites existing key" {
  186. init();
  187. const hash = hashing.hashKey("overwrite_key");
  188. const shard_idx = getShardIndex(hash);
  189. shards[shard_idx].rwlock.lock();
  190. _ = writeVolatile(hash, "overwrite_key", "first");
  191. shards[shard_idx].rwlock.unlock();
  192. shards[shard_idx].rwlock.lock();
  193. _ = writeVolatile(hash, "overwrite_key", "second");
  194. shards[shard_idx].rwlock.unlock();
  195. const val = read("overwrite_key") orelse return error.TestUnexpectedResult;
  196. try std.testing.expectEqualStrings("second", val);
  197. }
  198. test "read nonexistent key returns null" {
  199. init();
  200. try std.testing.expectEqual(@as(?[]const u8, null), read("no_such_key_xyz"));
  201. }
  202. test "deleteVolatile removes entry" {
  203. init();
  204. const hash = hashing.hashKey("del_key");
  205. const shard_idx = getShardIndex(hash);
  206. shards[shard_idx].rwlock.lock();
  207. _ = writeVolatile(hash, "del_key", "val");
  208. shards[shard_idx].rwlock.unlock();
  209. try std.testing.expect(read("del_key") != null);
  210. shards[shard_idx].rwlock.lock();
  211. const deleted = deleteVolatile(hash, "del_key");
  212. shards[shard_idx].rwlock.unlock();
  213. try std.testing.expect(deleted);
  214. try std.testing.expectEqual(@as(?[]const u8, null), read("del_key"));
  215. }
  216. test "deleteVolatile nonexistent key returns false" {
  217. init();
  218. const hash = hashing.hashKey("ghost_key");
  219. const shard_idx = getShardIndex(hash);
  220. shards[shard_idx].rwlock.lock();
  221. const deleted = deleteVolatile(hash, "ghost_key");
  222. shards[shard_idx].rwlock.unlock();
  223. try std.testing.expect(!deleted);
  224. }
  225. test "multiple keys in same shard" {
  226. init();
  227. // Write several keys and verify they don't interfere
  228. const keys = [_][]const u8{ "shard_a", "shard_b", "shard_c" };
  229. const vals = [_][]const u8{ "val_a", "val_b", "val_c" };
  230. for (keys, vals) |k, v| {
  231. const hash = hashing.hashKey(k);
  232. const shard_idx = getShardIndex(hash);
  233. shards[shard_idx].rwlock.lock();
  234. _ = writeVolatile(hash, k, v);
  235. shards[shard_idx].rwlock.unlock();
  236. }
  237. for (keys, vals) |k, v| {
  238. const val = read(k) orelse return error.TestUnexpectedResult;
  239. try std.testing.expectEqualStrings(v, val);
  240. }
  241. }
  242. test "empty key and value" {
  243. init();
  244. const hash = hashing.hashKey("");
  245. const shard_idx = getShardIndex(hash);
  246. shards[shard_idx].rwlock.lock();
  247. _ = writeVolatile(hash, "", "");
  248. shards[shard_idx].rwlock.unlock();
  249. const val = read("") orelse return error.TestUnexpectedResult;
  250. try std.testing.expectEqualStrings("", val);
  251. }
  252. test "getShardIndex stays in bounds" {
  253. try std.testing.expect(getShardIndex(0) < NUM_SHARDS);
  254. try std.testing.expect(getShardIndex(std.math.maxInt(u32)) < NUM_SHARDS);
  255. try std.testing.expect(getShardIndex(12345) < NUM_SHARDS);
  256. }