storage.zig 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  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. var entry_key: []const u8 = undefined;
  96. var entry_value: []const u8 = undefined;
  97. {
  98. shards[shard_idx].rwlock.lock();
  99. defer shards[shard_idx].rwlock.unlock();
  100. const entry = writeVolatile(hash, key, value) orelse return false;
  101. entry_key = entry.key;
  102. entry_value = entry.value;
  103. }
  104. index.insert(entry_key);
  105. persistence.persist('W', entry_key, entry_value);
  106. return true;
  107. }
  108. pub fn read(key: []const u8) ?[]const u8 {
  109. if (!shards_initialized) return null;
  110. const hash = hashing.hashKey(key);
  111. const shard_idx = getShardIndex(hash);
  112. shards[shard_idx].rwlock.lockShared();
  113. defer shards[shard_idx].rwlock.unlockShared();
  114. var current = shards[shard_idx].buckets[hash % shards[shard_idx].buckets.len];
  115. while (current) |entry| {
  116. if (entry.hash == hash and std.mem.eql(u8, entry.key, key)) {
  117. return entry.value;
  118. }
  119. current = entry.next;
  120. }
  121. return null;
  122. }
  123. pub fn deleteVolatile(hash: u32, key: []const u8) bool {
  124. if (!shards_initialized) return false;
  125. const shard_idx = getShardIndex(hash);
  126. const bucketIdx = hash % shards[shard_idx].buckets.len;
  127. const alloc = shards[shard_idx].allocator;
  128. var current = shards[shard_idx].buckets[bucketIdx];
  129. var prev: ?*Entry = null;
  130. while (current) |entry| {
  131. if (entry.hash == hash and std.mem.eql(u8, entry.key, key)) {
  132. if (prev) |p| {
  133. p.next = entry.next;
  134. } else {
  135. shards[shard_idx].buckets[bucketIdx] = entry.next;
  136. }
  137. alloc.free(entry.key);
  138. alloc.free(entry.value);
  139. alloc.destroy(entry);
  140. return true;
  141. }
  142. prev = entry;
  143. current = entry.next;
  144. }
  145. return false;
  146. }
  147. pub fn delete(key: []const u8) bool {
  148. const hash = hashing.hashKey(key);
  149. const shard_idx = getShardIndex(hash);
  150. const deleted = blk: {
  151. shards[shard_idx].rwlock.lock();
  152. defer shards[shard_idx].rwlock.unlock();
  153. break :blk deleteVolatile(hash, key);
  154. };
  155. if (deleted) {
  156. index.delete(key);
  157. persistence.persist('D', key, "");
  158. }
  159. return deleted;
  160. }
  161. // -- Tests --
  162. test "writeVolatile and read basic" {
  163. init();
  164. const hash = hashing.hashKey("test_key");
  165. const shard_idx = getShardIndex(hash);
  166. shards[shard_idx].rwlock.lock();
  167. _ = writeVolatile(hash, "test_key", "test_value");
  168. shards[shard_idx].rwlock.unlock();
  169. const val = read("test_key") orelse return error.TestUnexpectedResult;
  170. try std.testing.expectEqualStrings("test_value", val);
  171. }
  172. test "writeVolatile overwrites existing key" {
  173. init();
  174. const hash = hashing.hashKey("overwrite_key");
  175. const shard_idx = getShardIndex(hash);
  176. shards[shard_idx].rwlock.lock();
  177. _ = writeVolatile(hash, "overwrite_key", "first");
  178. shards[shard_idx].rwlock.unlock();
  179. shards[shard_idx].rwlock.lock();
  180. _ = writeVolatile(hash, "overwrite_key", "second");
  181. shards[shard_idx].rwlock.unlock();
  182. const val = read("overwrite_key") orelse return error.TestUnexpectedResult;
  183. try std.testing.expectEqualStrings("second", val);
  184. }
  185. test "read nonexistent key returns null" {
  186. init();
  187. try std.testing.expectEqual(@as(?[]const u8, null), read("no_such_key_xyz"));
  188. }
  189. test "deleteVolatile removes entry" {
  190. init();
  191. const hash = hashing.hashKey("del_key");
  192. const shard_idx = getShardIndex(hash);
  193. shards[shard_idx].rwlock.lock();
  194. _ = writeVolatile(hash, "del_key", "val");
  195. shards[shard_idx].rwlock.unlock();
  196. try std.testing.expect(read("del_key") != null);
  197. shards[shard_idx].rwlock.lock();
  198. const deleted = deleteVolatile(hash, "del_key");
  199. shards[shard_idx].rwlock.unlock();
  200. try std.testing.expect(deleted);
  201. try std.testing.expectEqual(@as(?[]const u8, null), read("del_key"));
  202. }
  203. test "deleteVolatile nonexistent key returns false" {
  204. init();
  205. const hash = hashing.hashKey("ghost_key");
  206. const shard_idx = getShardIndex(hash);
  207. shards[shard_idx].rwlock.lock();
  208. const deleted = deleteVolatile(hash, "ghost_key");
  209. shards[shard_idx].rwlock.unlock();
  210. try std.testing.expect(!deleted);
  211. }
  212. test "multiple keys in same shard" {
  213. init();
  214. // Write several keys and verify they don't interfere
  215. const keys = [_][]const u8{ "shard_a", "shard_b", "shard_c" };
  216. const vals = [_][]const u8{ "val_a", "val_b", "val_c" };
  217. for (keys, vals) |k, v| {
  218. const hash = hashing.hashKey(k);
  219. const shard_idx = getShardIndex(hash);
  220. shards[shard_idx].rwlock.lock();
  221. _ = writeVolatile(hash, k, v);
  222. shards[shard_idx].rwlock.unlock();
  223. }
  224. for (keys, vals) |k, v| {
  225. const val = read(k) orelse return error.TestUnexpectedResult;
  226. try std.testing.expectEqualStrings(v, val);
  227. }
  228. }
  229. test "empty key and value" {
  230. init();
  231. const hash = hashing.hashKey("");
  232. const shard_idx = getShardIndex(hash);
  233. shards[shard_idx].rwlock.lock();
  234. _ = writeVolatile(hash, "", "");
  235. shards[shard_idx].rwlock.unlock();
  236. const val = read("") orelse return error.TestUnexpectedResult;
  237. try std.testing.expectEqualStrings("", val);
  238. }
  239. test "getShardIndex stays in bounds" {
  240. try std.testing.expect(getShardIndex(0) < NUM_SHARDS);
  241. try std.testing.expect(getShardIndex(std.math.maxInt(u32)) < NUM_SHARDS);
  242. try std.testing.expect(getShardIndex(12345) < NUM_SHARDS);
  243. }