|
19 | 19 |
|
20 | 20 | #include <gtest/gtest.h> |
21 | 21 |
|
| 22 | +#include <atomic> |
22 | 23 | #include <chrono> |
23 | 24 | #include <cstdlib> |
24 | 25 | #include <filesystem> |
| 26 | +#include <mutex> |
25 | 27 | #include <string> |
26 | 28 | #include <thread> |
27 | 29 | #include <vector> |
@@ -1139,6 +1141,202 @@ TEST_F(TidesDBTest, TransactionResetAfterRollback) |
1139 | 1141 | } |
1140 | 1142 | } |
1141 | 1143 |
|
| 1144 | +// Commit hook test helpers |
| 1145 | +struct HookTestCtx |
| 1146 | +{ |
| 1147 | + std::atomic<int> callCount{0}; |
| 1148 | + std::atomic<int> totalOps{0}; |
| 1149 | + std::atomic<uint64_t> lastCommitSeq{0}; |
| 1150 | + std::mutex mu; |
| 1151 | + std::vector<std::string> capturedKeys; |
| 1152 | +}; |
| 1153 | + |
| 1154 | +static int testCommitHook(const tidesdb_commit_op_t* ops, int num_ops, uint64_t commit_seq, |
| 1155 | + void* ctx) |
| 1156 | +{ |
| 1157 | + auto* hctx = static_cast<HookTestCtx*>(ctx); |
| 1158 | + hctx->callCount.fetch_add(1); |
| 1159 | + hctx->totalOps.fetch_add(num_ops); |
| 1160 | + hctx->lastCommitSeq.store(commit_seq); |
| 1161 | + |
| 1162 | + std::lock_guard<std::mutex> lock(hctx->mu); |
| 1163 | + for (int i = 0; i < num_ops; ++i) |
| 1164 | + { |
| 1165 | + hctx->capturedKeys.emplace_back(reinterpret_cast<const char*>(ops[i].key), ops[i].key_size); |
| 1166 | + } |
| 1167 | + return 0; |
| 1168 | +} |
| 1169 | + |
| 1170 | +TEST_F(TidesDBTest, CommitHookBasic) |
| 1171 | +{ |
| 1172 | + tidesdb::TidesDB db(getConfig()); |
| 1173 | + |
| 1174 | + auto cfConfig = tidesdb::ColumnFamilyConfig::defaultConfig(); |
| 1175 | + db.createColumnFamily("test_cf", cfConfig); |
| 1176 | + |
| 1177 | + auto cf = db.getColumnFamily("test_cf"); |
| 1178 | + |
| 1179 | + HookTestCtx hookCtx; |
| 1180 | + cf.setCommitHook(testCommitHook, &hookCtx); |
| 1181 | + |
| 1182 | + // Commit a transaction -- hook should fire |
| 1183 | + { |
| 1184 | + auto txn = db.beginTransaction(); |
| 1185 | + txn.put(cf, "key1", "value1", -1); |
| 1186 | + txn.put(cf, "key2", "value2", -1); |
| 1187 | + txn.commit(); |
| 1188 | + } |
| 1189 | + |
| 1190 | + ASSERT_GE(hookCtx.callCount.load(), 1); |
| 1191 | + ASSERT_GE(hookCtx.totalOps.load(), 2); |
| 1192 | + ASSERT_GT(hookCtx.lastCommitSeq.load(), 0u); |
| 1193 | + |
| 1194 | + // Verify captured keys |
| 1195 | + { |
| 1196 | + std::lock_guard<std::mutex> lock(hookCtx.mu); |
| 1197 | + bool foundKey1 = false, foundKey2 = false; |
| 1198 | + for (const auto& k : hookCtx.capturedKeys) |
| 1199 | + { |
| 1200 | + if (k == "key1") foundKey1 = true; |
| 1201 | + if (k == "key2") foundKey2 = true; |
| 1202 | + } |
| 1203 | + ASSERT_TRUE(foundKey1); |
| 1204 | + ASSERT_TRUE(foundKey2); |
| 1205 | + } |
| 1206 | +} |
| 1207 | + |
| 1208 | +TEST_F(TidesDBTest, CommitHookClear) |
| 1209 | +{ |
| 1210 | + tidesdb::TidesDB db(getConfig()); |
| 1211 | + |
| 1212 | + auto cfConfig = tidesdb::ColumnFamilyConfig::defaultConfig(); |
| 1213 | + db.createColumnFamily("test_cf", cfConfig); |
| 1214 | + |
| 1215 | + auto cf = db.getColumnFamily("test_cf"); |
| 1216 | + |
| 1217 | + HookTestCtx hookCtx; |
| 1218 | + cf.setCommitHook(testCommitHook, &hookCtx); |
| 1219 | + |
| 1220 | + // First commit -- hook fires |
| 1221 | + { |
| 1222 | + auto txn = db.beginTransaction(); |
| 1223 | + txn.put(cf, "key1", "value1", -1); |
| 1224 | + txn.commit(); |
| 1225 | + } |
| 1226 | + |
| 1227 | + int countAfterFirst = hookCtx.callCount.load(); |
| 1228 | + ASSERT_GE(countAfterFirst, 1); |
| 1229 | + |
| 1230 | + // Clear the hook |
| 1231 | + cf.clearCommitHook(); |
| 1232 | + |
| 1233 | + // Second commit -- hook should NOT fire |
| 1234 | + { |
| 1235 | + auto txn = db.beginTransaction(); |
| 1236 | + txn.put(cf, "key2", "value2", -1); |
| 1237 | + txn.commit(); |
| 1238 | + } |
| 1239 | + |
| 1240 | + ASSERT_EQ(hookCtx.callCount.load(), countAfterFirst); |
| 1241 | +} |
| 1242 | + |
| 1243 | +TEST_F(TidesDBTest, CommitHookDeleteOps) |
| 1244 | +{ |
| 1245 | + tidesdb::TidesDB db(getConfig()); |
| 1246 | + |
| 1247 | + auto cfConfig = tidesdb::ColumnFamilyConfig::defaultConfig(); |
| 1248 | + db.createColumnFamily("test_cf", cfConfig); |
| 1249 | + |
| 1250 | + auto cf = db.getColumnFamily("test_cf"); |
| 1251 | + |
| 1252 | + // Insert data first |
| 1253 | + { |
| 1254 | + auto txn = db.beginTransaction(); |
| 1255 | + txn.put(cf, "del_key", "del_value", -1); |
| 1256 | + txn.commit(); |
| 1257 | + } |
| 1258 | + |
| 1259 | + HookTestCtx hookCtx; |
| 1260 | + cf.setCommitHook(testCommitHook, &hookCtx); |
| 1261 | + |
| 1262 | + // Delete the key -- hook should capture the delete |
| 1263 | + { |
| 1264 | + auto txn = db.beginTransaction(); |
| 1265 | + txn.del(cf, "del_key"); |
| 1266 | + txn.commit(); |
| 1267 | + } |
| 1268 | + |
| 1269 | + ASSERT_GE(hookCtx.callCount.load(), 1); |
| 1270 | + ASSERT_GE(hookCtx.totalOps.load(), 1); |
| 1271 | +} |
| 1272 | + |
| 1273 | +TEST_F(TidesDBTest, CommitHookMonotonicSeq) |
| 1274 | +{ |
| 1275 | + tidesdb::TidesDB db(getConfig()); |
| 1276 | + |
| 1277 | + auto cfConfig = tidesdb::ColumnFamilyConfig::defaultConfig(); |
| 1278 | + db.createColumnFamily("test_cf", cfConfig); |
| 1279 | + |
| 1280 | + auto cf = db.getColumnFamily("test_cf"); |
| 1281 | + |
| 1282 | + // Custom hook that records all commit sequences |
| 1283 | + struct SeqCtx |
| 1284 | + { |
| 1285 | + std::mutex mu; |
| 1286 | + std::vector<uint64_t> seqs; |
| 1287 | + } seqCtx; |
| 1288 | + |
| 1289 | + auto seqHook = [](const tidesdb_commit_op_t*, int, uint64_t commit_seq, void* ctx) -> int |
| 1290 | + { |
| 1291 | + auto* sctx = static_cast<SeqCtx*>(ctx); |
| 1292 | + std::lock_guard<std::mutex> lock(sctx->mu); |
| 1293 | + sctx->seqs.push_back(commit_seq); |
| 1294 | + return 0; |
| 1295 | + }; |
| 1296 | + |
| 1297 | + cf.setCommitHook(seqHook, &seqCtx); |
| 1298 | + |
| 1299 | + // Multiple commits |
| 1300 | + for (int i = 0; i < 5; ++i) |
| 1301 | + { |
| 1302 | + auto txn = db.beginTransaction(); |
| 1303 | + txn.put(cf, "key" + std::to_string(i), "value" + std::to_string(i), -1); |
| 1304 | + txn.commit(); |
| 1305 | + } |
| 1306 | + |
| 1307 | + // Verify monotonically increasing sequence numbers |
| 1308 | + std::lock_guard<std::mutex> lock(seqCtx.mu); |
| 1309 | + ASSERT_GE(seqCtx.seqs.size(), 5u); |
| 1310 | + for (size_t i = 1; i < seqCtx.seqs.size(); ++i) |
| 1311 | + { |
| 1312 | + ASSERT_GT(seqCtx.seqs[i], seqCtx.seqs[i - 1]); |
| 1313 | + } |
| 1314 | +} |
| 1315 | + |
| 1316 | +TEST_F(TidesDBTest, CommitHookViaConfig) |
| 1317 | +{ |
| 1318 | + tidesdb::TidesDB db(getConfig()); |
| 1319 | + |
| 1320 | + HookTestCtx hookCtx; |
| 1321 | + |
| 1322 | + auto cfConfig = tidesdb::ColumnFamilyConfig::defaultConfig(); |
| 1323 | + cfConfig.commitHookFn = testCommitHook; |
| 1324 | + cfConfig.commitHookCtx = &hookCtx; |
| 1325 | + db.createColumnFamily("test_cf", cfConfig); |
| 1326 | + |
| 1327 | + auto cf = db.getColumnFamily("test_cf"); |
| 1328 | + |
| 1329 | + // Commit -- hook should already be active from config |
| 1330 | + { |
| 1331 | + auto txn = db.beginTransaction(); |
| 1332 | + txn.put(cf, "key1", "value1", -1); |
| 1333 | + txn.commit(); |
| 1334 | + } |
| 1335 | + |
| 1336 | + ASSERT_GE(hookCtx.callCount.load(), 1); |
| 1337 | + ASSERT_GE(hookCtx.totalOps.load(), 1); |
| 1338 | +} |
| 1339 | + |
1142 | 1340 | int main(int argc, char** argv) |
1143 | 1341 | { |
1144 | 1342 | ::testing::InitGoogleTest(&argc, argv); |
|
0 commit comments