├── .gitignore ├── .package ├── LICENSE ├── README.md ├── ptx.c ├── ptx.h └── test.c /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | a.out 3 | *.o 4 | *.dSYM 5 | -------------------------------------------------------------------------------- /.package: -------------------------------------------------------------------------------- 1 | file ptx.c 2 | file ptx.h 3 | 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Joshua J Baker 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ptx 2 | 3 | Probabilistic transaction graph for optimistic concurrency control. 4 | 5 | A new method for serializing transactions. 6 | 7 | Rather than using timestamps or sequence numbers, ptx uses hashes that are 8 | stored in [growable bloom filters](https://github.com/tidwall/rhbloom), where 9 | each transaction is a node in a graph that links to dependent nodes. 10 | 11 | The graph model is inspired by the 12 | [SSI Algorithm](https://wiki.postgresql.org/wiki/Serializable) in Postgres, and 13 | works by looking out for certain wr/ww-dependencies and rw-antidependencies 14 | (aka conflicts) in transaction nodes that will cause anomolies such as read 15 | and write skews. 16 | 17 | The main idea is that you create a "graph" which represents a shared data 18 | resource (such as a database, table, collection, etc), from which you then 19 | instantiate transaction nodes using the "begin" operation, followed by providing 20 | "read" and "write" operations to that transaction along with a single hash 21 | representing what item in the shared data you're reading or writing. 22 | These hashed items can represent keys, tuples, ranges, or something else; pretty much whatever you want. 23 | When done with the transaction you call "commit" or "rollback". 24 | The "commit" operation will return true if the transaction can serialize and 25 | false if an anomoly was detected. 26 | 27 | Because ptx uses a bloom filter to store the hashes, it's possible to have 28 | false positives when detecting anomolies. The probability rate for false 29 | positives is a configurable option, as is the targeted number of elements. 30 | Default is 1,000,000 elements, 1% probability. 31 | 32 | This repository provides a working implementation written in C. It's designed 33 | to be small, fast, and easily embeddable. Should compile using any C99 compiler 34 | such as gcc, clang, and tcc. Includes webassembly (Emscripten / emcc) support. 35 | 36 | ## Example 37 | 38 | Here's an example that causes a simple write skew. 39 | 40 | ```c 41 | // Create a graph 42 | struct ptx_graph *graph = ptx_graph_new(0); 43 | 44 | // Create a transaction (T1) 45 | struct ptx_node *t1 = ptx_graph_begin(graph, 0); 46 | 47 | // Read an item (using the item's hash) (T1) 48 | ptx_node_read(t1, 0x1F19281); 49 | 50 | // Create a second transaction (T2) 51 | struct ptx_node *t2 = ptx_graph_begin(graph, 0); 52 | 53 | // Read the same item (T2) 54 | ptx_node_read(t2, 0x1F19281); 55 | 56 | // Now write the item (T1) 57 | ptx_node_write(t1, 0x1F19281); 58 | 59 | // And commit the transaction (T1) 60 | // This will succeed. 61 | assert(ptx_node_commit(t1) == true); 62 | 63 | // Now write the in the other transaction (T2) 64 | ptx_node_write(t2, 0x1F19281); 65 | 66 | // And try to commit that transaction (T2) 67 | // This will fail. 68 | assert(ptx_node_commit(t2) == false); 69 | 70 | // Finally free the graph to when your are done with it. 71 | ptx_graph_free(graph); 72 | ``` 73 | 74 | ## API 75 | 76 | ```c 77 | struct ptx_graph; 78 | struct ptx_node; 79 | 80 | struct ptx_graph_opts { 81 | void*(*malloc)(size_t); // custom allocator 82 | void(*free)(void*); // custom allocator 83 | size_t n; // bloom filter: number of elements (default 1,000,000) 84 | double p; // bloom filter: false positive rate (default 1%) 85 | int autogc; // automatic gc cycle, set -1 to disable. (default: 1000) 86 | }; 87 | 88 | // Create a new graph. 89 | // Returns NULL if out of memory. 90 | struct ptx_graph *ptx_graph_new(struct ptx_graph_opts*); 91 | 92 | // Free the graph and all child transactions 93 | void ptx_graph_free(struct ptx_graph *graph); 94 | 95 | // Begin a new transaction. 96 | // Returns NULL if out of memory. 97 | struct ptx_node *ptx_graph_begin(struct ptx_graph *graph, void *opt); 98 | 99 | // Read an item using the item's hash 100 | void ptx_node_read(struct ptx_node *node, uint64_t hash); 101 | 102 | // Write an item using the item's hash 103 | void ptx_node_write(struct ptx_node *node, uint64_t hash); 104 | 105 | // Rollback a transaction 106 | // The transaction node should not be used again after this call. 107 | void ptx_node_rollback(struct ptx_node *node); 108 | 109 | // Commit a transaction. Returns false if failure to serialize 110 | // The transaction node should not be used again after this call. 111 | bool ptx_node_commit(struct ptx_node *node); 112 | 113 | // Returns true if last ptx_node_commit() failure was due to out of memory 114 | bool ptx_oom(void); 115 | 116 | // Debug: set a label for the transaction 117 | void ptx_node_setlabel(struct ptx_node *node, const char *label); 118 | 119 | // Debug: return the transaction label 120 | const char *ptx_node_label(struct ptx_node *node); 121 | 122 | // Debug: print the entire graph to stdout 123 | void ptx_graph_print(struct ptx_graph *graph, bool withedges); 124 | 125 | // Debug: call a garbage collection cycle now 126 | void ptx_graph_gc(struct ptx_graph *graph); 127 | ``` 128 | 129 | ## Links 130 | 131 | - https://en.wikipedia.org/wiki/Optimistic_concurrency_control 132 | - https://github.com/tidwall/rhbloom 133 | - https://wiki.postgresql.org/wiki/SSI 134 | - https://drkp.net/papers/ssi-vldb12.pdf 135 | - https://jepsen.io/analyses/postgresql-12.3 136 | - https://wiki.postgresql.org/wiki/Serializable 137 | -------------------------------------------------------------------------------- /ptx.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #ifndef PTX_EXTERN 6 | #define PTX_EXTERN 7 | #endif 8 | 9 | struct ptx_graph; 10 | struct ptx_node; 11 | 12 | struct ptx_graph_opts { 13 | void*(*malloc)(size_t); // custom allocator 14 | void(*free)(void*); // custom allocator 15 | size_t n; // number bloom filter elements (default 1,000,000) 16 | double p; // false positive rate (default 1%) 17 | int autogc; // automatic gc cycle, set -1 to disable. (default: 1000) 18 | }; 19 | 20 | PTX_EXTERN struct ptx_graph *ptx_graph_new(struct ptx_graph_opts*); 21 | PTX_EXTERN void ptx_graph_free(struct ptx_graph *graph); 22 | PTX_EXTERN void ptx_graph_gc(struct ptx_graph *graph); 23 | PTX_EXTERN struct ptx_node *ptx_graph_begin(struct ptx_graph *graph, void *opt); 24 | PTX_EXTERN void ptx_node_setlabel(struct ptx_node *node, const char *label); 25 | PTX_EXTERN const char *ptx_node_label(struct ptx_node *node); 26 | PTX_EXTERN void ptx_node_read(struct ptx_node *node, uint64_t hash); 27 | PTX_EXTERN void ptx_node_write(struct ptx_node *node, uint64_t hash); 28 | PTX_EXTERN void ptx_node_rollback(struct ptx_node *node); 29 | PTX_EXTERN bool ptx_node_commit(struct ptx_node *node); 30 | PTX_EXTERN bool ptx_oom(void); 31 | PTX_EXTERN void ptx_graph_print(struct ptx_graph *graph, bool withedges); 32 | PTX_EXTERN void ptx_graph_print_state(struct ptx_graph *graph, char output[]); 33 | 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | 40 | #define PTX_TRACKINS 41 | 42 | #define PTX_DEFAULT_N 1000000 43 | #define PTX_DEFAULT_P 0.01 44 | #define PTC_DEFAULT_AUTOGC 1000 45 | 46 | #define PTX_ACTIVE 0 47 | #define PTX_COMMITTED 1 48 | #define PTX_ROLLEDBACK 2 49 | #define PTX_NOMEM 3 50 | #define PTX_RELEASED 4 51 | 52 | #define PTX_WR 1 53 | #define PTX_WW 2 54 | #define PTX_RW 4 55 | 56 | struct ptx_edge { 57 | uint16_t dib; // bucket distance (robinhood hashtable) 58 | uint16_t kind; // edge kind: TXWR, TXWW, TXRW 59 | struct ptx_node *node; // the node 60 | }; 61 | 62 | struct ptx_edgemap { 63 | struct ptx_edge *buckets; 64 | size_t count; 65 | size_t nbuckets; 66 | }; 67 | 68 | struct ptx_hashset { 69 | // hashtable fields 70 | size_t nbuckets; 71 | size_t count; 72 | uint64_t *buckets; 73 | uint64_t buckets0[4]; // 74 | // bloom fields 75 | size_t k; // number of bits per key 76 | size_t m; // number of bits total 77 | uint8_t *bits; // bloom bits 78 | }; 79 | 80 | struct ptx_node { 81 | struct ptx_node *prev; 82 | struct ptx_node *next; 83 | int state; 84 | uint64_t ident; 85 | struct ptx_graph *graph; // root graph 86 | bool reached; 87 | bool nomem; 88 | 89 | bool hasdeps; 90 | bool hasreads; 91 | bool haswrites; 92 | 93 | 94 | struct ptx_edgemap outs; // Edges that join this node to another. 95 | #ifdef PTX_TRACKINS 96 | struct ptx_edgemap ins; // Edges that join this node to another. 97 | #endif 98 | struct ptx_hashset reads; 99 | struct ptx_hashset writes; 100 | char label[32]; 101 | }; 102 | 103 | struct ptx_graph { 104 | struct ptx_node head; 105 | struct ptx_node tail; 106 | uint64_t ident; // ident counter 107 | int gccounter; // gc counter 108 | int autogc; // 109 | void*(*malloc)(size_t); 110 | void(*free)(void*); 111 | size_t n; // number bloom filter elements (default 1,000,000) 112 | double p; // false positive rate (default 1%) 113 | }; 114 | 115 | static __thread bool _ptx_oom = false; 116 | 117 | bool ptx_oom(void) { 118 | return _ptx_oom; 119 | } 120 | 121 | static void ptx_hashset_init(struct ptx_hashset *set, size_t n, double p) { 122 | memset(set, 0, sizeof(struct ptx_hashset)); 123 | // Hashtable 124 | set->nbuckets = sizeof(set->buckets0)/8; 125 | set->buckets = set->buckets0; 126 | // Bloom filter 127 | if (n < 16) { 128 | n = 16; 129 | } 130 | // Calculate the total number of bits needed 131 | size_t m = n * log(p) / log(1 / pow(2, log(2))); 132 | // Calculate the bits per key 133 | size_t k = round(((double)m / (double)(n)) * log(2)); 134 | // Adjust the number of bit to power of two 135 | set->m = 2; 136 | while (set->m < m) { 137 | set->m *= 2; 138 | } 139 | set->k = round((double)m / (double)set->m * (double)k); 140 | } 141 | 142 | static void ptx_hashset_free(struct ptx_graph *graph, struct ptx_hashset *set) { 143 | if (set->buckets != set->buckets0) { 144 | graph->free(set->buckets); 145 | } 146 | if (set->bits) { 147 | graph->free(set->bits); 148 | } 149 | } 150 | 151 | static uint64_t ptx_hashof(uint64_t x) { 152 | return x << 8 >> 8; 153 | } 154 | static uint8_t ptx_dibof(uint64_t x) { 155 | return x >> 56; 156 | } 157 | static uint64_t ptx_sethashdib(uint64_t hash, uint8_t dib) { 158 | return ptx_hashof(hash) | ((uint64_t)dib << 56); 159 | } 160 | 161 | static bool ptx_testadd(struct ptx_hashset *set, uint64_t hash, 162 | bool add) 163 | { 164 | // We only want the 56-bit hash in order to match correcly with the 165 | // robinhood entries, upon upgrade. 166 | hash = ptx_hashof(hash); 167 | // Add or check each bit 168 | size_t i = 0; 169 | size_t j = hash & (set->m-1); 170 | while (1) { 171 | if (add) { 172 | set->bits[j>>3] |= add<<(j&7); 173 | } else if (!((set->bits[j>>3]>>(j&7))&1)) { 174 | return false; 175 | } 176 | if (i == set->k-1) { 177 | break; 178 | } 179 | // Pick the next bit. 180 | // Use part of the mix13 forumula to help get a more randomized value. 181 | // https://zimbry.blogspot.com/2011/09/better-bit-mixing-improving-on.html 182 | hash *= UINT64_C(0x94d049bb133111eb); 183 | hash ^= hash >> 31; 184 | j = hash & (set->m-1); 185 | i++; 186 | } 187 | return true; 188 | } 189 | 190 | static void ptx_add0(struct ptx_hashset *set, uint64_t hash) { 191 | hash = ptx_hashof(hash); 192 | uint8_t dib = 1; 193 | size_t i = hash & (set->nbuckets-1); 194 | while (1) { 195 | if (ptx_dibof(set->buckets[i]) == 0) { 196 | set->buckets[i] = ptx_sethashdib(hash, dib); 197 | set->count++; 198 | return; 199 | } 200 | if (ptx_dibof(set->buckets[i]) < dib) { 201 | uint64_t tmp = set->buckets[i]; 202 | set->buckets[i] = ptx_sethashdib(hash, dib); 203 | hash = ptx_hashof(tmp); 204 | dib = ptx_dibof(tmp); 205 | } 206 | if (ptx_hashof(set->buckets[i]) == hash) { 207 | return; 208 | } 209 | dib++; 210 | i = (i + 1) & (set->nbuckets-1); 211 | } 212 | } 213 | 214 | static bool ptx_grow(struct ptx_graph *graph, struct ptx_hashset *set) { 215 | uint64_t *buckets_old = set->buckets; 216 | size_t nbuckets_old = set->nbuckets; 217 | if (set->nbuckets*2*8 >= set->m/8) { 218 | // Upgrade to bloom filter 219 | set->bits = graph->malloc(set->m/8); 220 | if (!set->bits) { 221 | return false; 222 | } 223 | set->nbuckets *= 2; 224 | memset(set->bits, 0, set->m/8); 225 | set->count = 0; 226 | set->nbuckets = 0; 227 | set->buckets = set->buckets0; 228 | for (size_t i = 0; i < nbuckets_old; i++) { 229 | if (ptx_dibof(buckets_old[i])) { 230 | ptx_testadd(set, buckets_old[i], true); 231 | } 232 | } 233 | } else { 234 | set->buckets = graph->malloc(set->nbuckets*2*8); 235 | if (set->buckets) { 236 | return false; 237 | } 238 | set->nbuckets *= 2; 239 | memset(set->buckets, 0, set->nbuckets * 8); 240 | for (size_t i = 0; i < nbuckets_old; i++) { 241 | if (ptx_dibof(buckets_old[i])) { 242 | ptx_add0(set, buckets_old[i]); 243 | } 244 | } 245 | } 246 | if (buckets_old != set->buckets0) { 247 | graph->free(buckets_old); 248 | } 249 | return true; 250 | } 251 | 252 | static bool ptx_hashset_add(struct ptx_graph *graph, struct ptx_hashset *set, 253 | uint64_t hash) 254 | { 255 | do { 256 | if (set->bits) { 257 | ptx_testadd(set, hash, true); 258 | } else if (set->count < set->nbuckets >> 1) { 259 | ptx_add0(set, hash); 260 | } else { 261 | if (!ptx_grow(graph, set)) { 262 | return false; 263 | } 264 | continue; 265 | } 266 | } while (0); 267 | return true; 268 | } 269 | 270 | static bool ptx_hashset_test(struct ptx_hashset *set, uint64_t hash) { 271 | if (set->bits) { 272 | return ptx_testadd(set, hash, false); 273 | } 274 | hash = ptx_hashof(hash); 275 | uint8_t dib = 1; 276 | size_t i = hash & (set->nbuckets-1); 277 | while (1) { 278 | if (ptx_hashof(set->buckets[i]) == hash) { 279 | return true; 280 | } 281 | if (ptx_dibof(set->buckets[i]) < dib) { 282 | return false; 283 | } 284 | dib++; 285 | i = (i + 1) & (set->nbuckets-1); 286 | } 287 | } 288 | 289 | // Returns true if edgemap is empty 290 | static bool ptx_hashset_empty(struct ptx_hashset *set) { 291 | return set->bits == 0 && set->count == 0; 292 | } 293 | 294 | // Returns the number of items in edgemap 295 | static size_t ptx_edgemap_count(struct ptx_edgemap *map) { 296 | return map->count; 297 | } 298 | 299 | // Free the edgemap 300 | static void ptx_edgemap_free(struct ptx_graph *graph, struct ptx_edgemap *map) { 301 | if (map->buckets) { 302 | graph->free(map->buckets); 303 | } 304 | } 305 | 306 | // Iterate over all edges. Returns NULL when done. 307 | // Example: 308 | // size_t pidx = 0; 309 | // struct ptx_edge *edge = ptx_edgemap_iter(map, &pidx); 310 | // while (edge) { 311 | // edge = ptx_edgemap_iter(map, &pidx); 312 | // } 313 | static struct ptx_edge *ptx_edgemap_iter(struct ptx_edgemap *map, size_t *pidx){ 314 | while (1) { 315 | if (*pidx >= map->nbuckets) { 316 | return 0; 317 | } 318 | struct ptx_edge *edge = &map->buckets[(*pidx)++]; 319 | if (edge->dib > 0) { 320 | return edge; 321 | } 322 | } 323 | } 324 | 325 | struct ptx_graph *ptx_graph_new(struct ptx_graph_opts *opts) { 326 | void*(*_malloc)(size_t) = opts ? opts->malloc : 0; 327 | void(*_free)(void*) = opts ? opts->free : 0; 328 | size_t n = opts ? opts->n : 0; 329 | double p = opts ? opts->p : 0; 330 | int autogc = opts ? opts->autogc : 0; 331 | _malloc = _malloc ? _malloc : malloc; 332 | _free = _free ? _free : free; 333 | n = n > 0 ? n : PTX_DEFAULT_N; 334 | p = p > 0 && isfinite(p) ? p : PTX_DEFAULT_P; 335 | autogc = autogc > 0 ? autogc : PTC_DEFAULT_AUTOGC; 336 | struct ptx_graph *graph = _malloc(sizeof(struct ptx_graph)); 337 | if (!graph) { 338 | return 0; 339 | } 340 | memset(graph, 0, sizeof(struct ptx_graph)); 341 | graph->malloc = _malloc; 342 | graph->free = _free; 343 | graph->n = n; 344 | graph->p = p; 345 | graph->head.next = &graph->tail; 346 | graph->tail.prev = &graph->head; 347 | return graph; 348 | } 349 | 350 | static void ptx_node_unlink(struct ptx_node *node) { 351 | if (node->prev) { 352 | node->prev->next = node->next; 353 | node->next->prev = node->prev; 354 | node->prev = 0; 355 | node->next = 0; 356 | } 357 | node->graph = 0; 358 | } 359 | 360 | static void ptx_node_free(struct ptx_node *node) { 361 | struct ptx_graph *graph = node->graph; 362 | ptx_node_unlink(node); 363 | ptx_hashset_free(graph, &node->reads); 364 | ptx_hashset_free(graph, &node->writes); 365 | #ifdef PTX_TRACKINS 366 | ptx_edgemap_free(graph, &node->ins); 367 | #endif 368 | ptx_edgemap_free(graph, &node->outs); 369 | graph->free(node); 370 | } 371 | 372 | void ptx_graph_free(struct ptx_graph *graph) { 373 | // Run the garbage collector. 374 | ptx_graph_gc(graph); 375 | // Any remaining nodes mush be rolled back. 376 | while (graph->head.next != &graph->tail) { 377 | struct ptx_node *node = graph->head.next; 378 | ptx_node_unlink(node); 379 | if (node->state == PTX_ACTIVE) { 380 | node->state = PTX_RELEASED; 381 | } 382 | } 383 | graph->free(graph); 384 | } 385 | 386 | 387 | static void ptx_node_gcmark(struct ptx_node *node) { 388 | if (!node->reached) { 389 | node->reached = true; 390 | size_t pidx = 0; 391 | struct ptx_edge *edge = ptx_edgemap_iter(&node->outs, &pidx); 392 | while (edge) { 393 | ptx_node_gcmark(edge->node); 394 | edge = ptx_edgemap_iter(&node->outs, &pidx); 395 | } 396 | } 397 | } 398 | 399 | void ptx_graph_gc(struct ptx_graph *graph) { 400 | // Mark. Look for reached nodes. 401 | struct ptx_node *node = graph->head.next; 402 | while (node != &graph->tail) { 403 | if (node->state == PTX_ACTIVE) { 404 | ptx_node_gcmark(node); 405 | } 406 | node = node->next; 407 | } 408 | // Sweep. Free unreached nodes. 409 | node = graph->head.next; 410 | while (node != &graph->tail) { 411 | struct ptx_node *next = node->next; 412 | if (!node->reached) { 413 | ptx_node_free(node); 414 | } else { 415 | node->reached = 0; 416 | } 417 | node = next; 418 | } 419 | } 420 | 421 | static void ptx_graph_autogc(struct ptx_graph *graph) { 422 | if (graph->gccounter >= graph->autogc) { 423 | graph->gccounter = 0; 424 | ptx_graph_gc(graph); 425 | } 426 | } 427 | 428 | struct ptx_node *ptx_graph_begin(struct ptx_graph *graph, void *opt) { 429 | (void)opt; // unused atm 430 | struct ptx_node *node = graph->malloc(sizeof(struct ptx_node)); 431 | if (!node) { 432 | return 0; 433 | } 434 | memset(node, 0, sizeof(struct ptx_node)); 435 | ptx_hashset_init(&node->reads, graph->n, graph->p); 436 | ptx_hashset_init(&node->writes, graph->n, graph->p); 437 | node->state = PTX_ACTIVE; 438 | node->graph = graph; 439 | graph->tail.prev->next = node; 440 | node->prev = graph->tail.prev; 441 | node->next = &graph->tail; 442 | graph->tail.prev = node; 443 | node->ident = ++graph->ident; 444 | ptx_node_setlabel(node, 0); 445 | return node; 446 | } 447 | 448 | void ptx_node_setlabel(struct ptx_node *node, const char *label) { 449 | if (label) { 450 | snprintf(node->label, sizeof(node->label), "%s", label); 451 | } else { 452 | snprintf(node->label, sizeof(node->label), "T(%" PRIu64 ")", 453 | node->ident); 454 | } 455 | } 456 | 457 | const char *ptx_node_label(struct ptx_node *node) { 458 | return node->label; 459 | } 460 | 461 | static void ptx_node_deactivate(struct ptx_node *node, int state) { 462 | node->state = state; 463 | if (node->graph->autogc > 0) { 464 | node->graph->gccounter++; 465 | if (ptx_edgemap_count(&node->outs) == 0 && !node->hasdeps) { 466 | ptx_node_free(node); 467 | } 468 | ptx_graph_autogc(node->graph); 469 | } 470 | } 471 | 472 | void ptx_node_rollback(struct ptx_node *node) { 473 | assert(node->state == PTX_ACTIVE || node->state == PTX_NOMEM); 474 | ptx_node_deactivate(node, PTX_ROLLEDBACK); 475 | } 476 | 477 | bool ptx_node_commit(struct ptx_node *node) { 478 | assert(node->state == PTX_ACTIVE || node->state == PTX_NOMEM); 479 | if (node->state == PTX_NOMEM) { 480 | _ptx_oom = true; 481 | ptx_node_deactivate(node, PTX_ROLLEDBACK); 482 | return false; 483 | } 484 | _ptx_oom = false; 485 | bool abort = false; 486 | size_t pidx = 0; 487 | struct ptx_edge *edge = ptx_edgemap_iter(&node->outs, &pidx); 488 | while (edge) { 489 | if (edge->node->state == PTX_COMMITTED && edge->node->haswrites) { 490 | abort = true; 491 | break; 492 | } 493 | edge = ptx_edgemap_iter(&node->outs, &pidx); 494 | } 495 | if (abort) { 496 | ptx_node_deactivate(node, PTX_ROLLEDBACK); 497 | return false; 498 | } else { 499 | ptx_node_deactivate(node, PTX_COMMITTED); 500 | return true; 501 | } 502 | } 503 | 504 | static bool ptx_edge_equal(struct ptx_edge *a, struct ptx_edge *b) { 505 | return a->node->ident == b->node->ident && a->kind == b->kind; 506 | } 507 | 508 | 509 | // Add the edge by performing Robin-hood hashing. 510 | // This is an intermediate operation and should not be called directly. 511 | static void ptx_edgemap_add0(struct ptx_edgemap *map, struct ptx_edge edge) { 512 | edge.dib = 1; 513 | size_t i = edge.node->ident & (map->nbuckets-1); 514 | while (1) { 515 | if (map->buckets[i].dib == 0) { 516 | map->buckets[i] = edge; 517 | map->count++; 518 | break; 519 | } 520 | if (ptx_edge_equal(&map->buckets[i], &edge)) { 521 | return; 522 | } 523 | if (map->buckets[i].dib < edge.dib) { 524 | struct ptx_edge tmp = map->buckets[i]; 525 | map->buckets[i] = edge; 526 | edge = tmp; 527 | } 528 | edge.dib++; 529 | i = (i + 1) & (map->nbuckets-1); 530 | } 531 | } 532 | 533 | // Double the map capacity. 534 | // Return true on Success, or false on Out of memory. 535 | static bool ptx_edgemap_grow(struct ptx_graph *graph, struct ptx_edgemap *map) { 536 | struct ptx_edge *buckets0 = map->buckets; 537 | size_t nbuckets0 = map->nbuckets; 538 | size_t nbuckets1 = map->nbuckets == 0 ? 2 : map->nbuckets * 2; 539 | struct ptx_edge *bucket1 = graph->malloc(sizeof(struct ptx_edge)*nbuckets1); 540 | if (!bucket1) { 541 | return false; 542 | } 543 | memset(bucket1, 0, sizeof(struct ptx_edge)*nbuckets1); 544 | map->buckets = bucket1; 545 | map->nbuckets = nbuckets1; 546 | map->count = 0; 547 | for (size_t i = 0; i < nbuckets0; i++) { 548 | if (buckets0[i].dib) { 549 | ptx_edgemap_add0(map, buckets0[i]); 550 | } 551 | } 552 | if (buckets0) { 553 | graph->free(buckets0); 554 | } 555 | return true; 556 | } 557 | 558 | 559 | // Adds an edge to the map. 560 | // Return true on Success, or false on Out of memory. 561 | static bool ptx_edgemap_add(struct ptx_edgemap *map, struct ptx_node *node, 562 | int kind) 563 | { 564 | if (map->count == map->nbuckets / 2) { 565 | if (!ptx_edgemap_grow(node->graph, map)) { 566 | return false; 567 | } 568 | } 569 | struct ptx_edge edge = { 570 | .kind = (int16_t)kind, 571 | .node = node, 572 | }; 573 | ptx_edgemap_add0(map, edge); 574 | return true; 575 | } 576 | 577 | // add an edge dependency from node-a to node-b. 578 | static bool ptx_node_adddep(struct ptx_node *a, struct ptx_node *b, int kind) { 579 | #ifdef PTX_TRACKINS 580 | if (!ptx_edgemap_add(&b->ins, a, kind)) { 581 | return false; 582 | } 583 | #endif 584 | if (!ptx_edgemap_add(&a->outs, b, kind)) { 585 | return false; 586 | } 587 | b->hasdeps = true; 588 | return true; 589 | } 590 | 591 | void ptx_node_read(struct ptx_node *node, uint64_t hash) { 592 | // The node can only be in ACTIVE or NOMEM state 593 | assert(node->state == PTX_ACTIVE || node->state == PTX_NOMEM); 594 | if (node->state == PTX_NOMEM) { 595 | return; 596 | } 597 | // Add the read to the current node 598 | if (!ptx_hashset_add(node->graph, &node->reads, hash)) { 599 | node->state = PTX_NOMEM; 600 | return; 601 | } 602 | node->hasreads = true; 603 | // Search for nodes that have written the same hash 604 | struct ptx_node *other = node->graph->head.next; 605 | while (other != &node->graph->tail) { 606 | if (other != node) { 607 | if (ptx_hashset_test(&other->writes, hash)) { 608 | if (!ptx_node_adddep(other, node, PTX_WR)) { 609 | node->state = PTX_NOMEM; 610 | return; 611 | } 612 | } 613 | } 614 | other = other->next; 615 | } 616 | } 617 | 618 | void ptx_node_write(struct ptx_node *node, uint64_t hash) { 619 | // The node can only be in ACTIVE or NOMEM state 620 | assert(node->state == PTX_ACTIVE || node->state == PTX_NOMEM); 621 | if (node->state == PTX_NOMEM) { 622 | return; 623 | } 624 | // Add the write to the current node 625 | if (!ptx_hashset_add(node->graph, &node->writes, hash)) { 626 | node->state = PTX_NOMEM; 627 | return; 628 | } 629 | node->haswrites = true; 630 | // Search for nodes that have read or written the same hash. 631 | struct ptx_node *other = node->graph->head.next; 632 | while (other != &node->graph->tail) { 633 | if (other != node) { 634 | if (ptx_hashset_test(&other->reads, hash)) { 635 | if (!ptx_node_adddep(other, node, PTX_RW)) { 636 | node->state = PTX_NOMEM; 637 | return; 638 | } 639 | } 640 | if (ptx_hashset_test(&other->writes, hash)) { 641 | if (!ptx_node_adddep(other, node, PTX_WW)) { 642 | node->state = PTX_NOMEM; 643 | return; 644 | } 645 | if (!ptx_node_adddep(node, other, PTX_WW)) { 646 | node->state = PTX_NOMEM; 647 | return; 648 | } 649 | } 650 | } 651 | other = other->next; 652 | } 653 | } 654 | 655 | void ptx_graph_print(struct ptx_graph *graph, bool withedges) { 656 | struct ptx_node *node = graph->head.next; 657 | char T1[32]; 658 | char T2[32]; 659 | while (node != &graph->tail) { 660 | snprintf(T1, sizeof(T1), "%s", node->label); 661 | printf("%s", T1); 662 | if (node->state == PTX_ACTIVE) { 663 | printf(" \033[1mACTIVE\033[m "); 664 | } else if (node->state == PTX_COMMITTED) { 665 | printf(" \033[1;32mCOMMIT\033[m "); 666 | } else if (node->state == PTX_ROLLEDBACK) { 667 | printf(" \033[1;31mROLLBACK\033[m "); 668 | } 669 | #ifdef PTX_TRACKINS 670 | printf("(%d ins, %d outs)", (int)ptx_edgemap_count(&node->ins), 671 | #else 672 | printf("(%d outs)", 673 | #endif 674 | (int)ptx_edgemap_count(&node->outs)); 675 | if (ptx_hashset_empty(&node->writes)) { 676 | printf(" \033[2m\033[m"); 677 | } 678 | printf("\n"); 679 | if (withedges) { 680 | size_t pidx; 681 | struct ptx_edge *edge; 682 | printf("\033[1m"); 683 | pidx = 0; 684 | edge = ptx_edgemap_iter(&node->outs, &pidx); 685 | while (edge) { 686 | snprintf(T2, sizeof(T2), "%s", edge->node->label); 687 | if (edge->kind == PTX_RW) { 688 | printf(" %s ----(rw)---> %s\n", T1, T2); 689 | } 690 | if (edge->kind == PTX_WR) { 691 | printf(" %s ----(wr)---> %s\n", T1, T2); 692 | } 693 | if (edge->kind == PTX_WW) { 694 | printf(" %s ----(ww)---> %s\n", T1, T2); 695 | } 696 | edge = ptx_edgemap_iter(&node->outs, &pidx); 697 | } 698 | printf("\033[m"); 699 | #ifdef PTX_TRACKINS 700 | printf("\033[2m\033[1;30m"); 701 | pidx = 0; 702 | edge = ptx_edgemap_iter(&node->ins, &pidx); 703 | while (edge) { 704 | snprintf(T2, sizeof(T2), "%s", edge->node->label); 705 | if (edge->kind == PTX_RW) { 706 | printf(" %s <---(rw)---- %s\n", T1, T2); 707 | } 708 | if (edge->kind == PTX_WR) { 709 | printf(" %s <---(wr)---- %s\n", T1, T2); 710 | } 711 | if (edge->kind == PTX_WW) { 712 | printf(" %s <---(ww)---- %s\n", T1, T2); 713 | } 714 | edge = ptx_edgemap_iter(&node->ins, &pidx); 715 | } 716 | printf("\033[m"); 717 | #endif 718 | } 719 | node = node->next; 720 | } 721 | } 722 | 723 | static const char *ptx_strstate(int status) { 724 | switch (status) { 725 | case PTX_ACTIVE: return "ACTIVE"; 726 | case PTX_COMMITTED: return "COMMIT"; 727 | case PTX_ROLLEDBACK: return "ROLLBACK"; 728 | case PTX_NOMEM: return "NOMEM"; 729 | case PTX_RELEASED: return "RELEASED"; 730 | default: return "UNKNOWN"; 731 | } 732 | } 733 | 734 | void ptx_graph_print_state(struct ptx_graph *graph, char output[]) { 735 | int i = 0; 736 | char buf[128] = ""; 737 | output[0] = 0; 738 | struct ptx_node *node = graph->head.next; 739 | while (node != &graph->tail) { 740 | if (i > 0) { 741 | strcat(output, ", "); 742 | } 743 | snprintf(buf, sizeof(buf), "%s %s", node->label, 744 | ptx_strstate(node->state)); 745 | strcat(output, buf); 746 | node = node->next; 747 | i++; 748 | } 749 | } 750 | -------------------------------------------------------------------------------- /ptx.h: -------------------------------------------------------------------------------- 1 | // https://github.com/tidwall/ptx 2 | // 3 | // Copyright 2024 Joshua J Baker. All rights reserved. 4 | // Use of this source code is governed by an MIT-style 5 | // license that can be found in the LICENSE file. 6 | 7 | #ifndef PTX_H 8 | #define PTX_H 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | struct ptx_graph; 15 | struct ptx_node; 16 | 17 | struct ptx_graph_opts { 18 | void*(*malloc)(size_t); // custom allocator 19 | void(*free)(void*); // custom allocator 20 | size_t n; // bloom filter: number of elements (default 1,000,000) 21 | double p; // bloom filter: false positive rate (default 1%) 22 | int autogc; // automatic gc cycle, set -1 to disable. (default: 1000) 23 | }; 24 | 25 | // Create a new graph. 26 | // Returns NULL if out of memory. 27 | struct ptx_graph *ptx_graph_new(struct ptx_graph_opts*); 28 | 29 | // Free the graph and all child transactions 30 | void ptx_graph_free(struct ptx_graph *graph); 31 | 32 | // Begin a new transaction. 33 | // Returns NULL if out of memory. 34 | struct ptx_node *ptx_graph_begin(struct ptx_graph *graph, void *opt); 35 | 36 | // Read an item using the item's hash 37 | void ptx_node_read(struct ptx_node *node, uint64_t hash); 38 | 39 | // Write an item using the item's hash 40 | void ptx_node_write(struct ptx_node *node, uint64_t hash); 41 | 42 | // Rollback a transaction 43 | // The transaction node should not be used again after this call. 44 | void ptx_node_rollback(struct ptx_node *node); 45 | 46 | // Commit a transaction. Returns false if failure to serialize 47 | // The transaction node should not be used again after this call. 48 | bool ptx_node_commit(struct ptx_node *node); 49 | 50 | // Returns true if last ptx_node_commit() failure was due to out of memory 51 | bool ptx_oom(void); 52 | 53 | // Debug: set a label for the transaction 54 | void ptx_node_setlabel(struct ptx_node *node, const char *label); 55 | 56 | // Debug: return the transaction label 57 | const char *ptx_node_label(struct ptx_node *node); 58 | 59 | // Debug: print the entire graph to stdout 60 | void ptx_graph_print(struct ptx_graph *graph, bool withedges); 61 | 62 | // Debug: call a garbage collection cycle now 63 | void ptx_graph_gc(struct ptx_graph *graph); 64 | 65 | #endif 66 | -------------------------------------------------------------------------------- /test.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "ptx.h" 5 | 6 | void ptx_graph_print_state(struct ptx_graph *graph, char output[]); 7 | 8 | static size_t _nallocs = 0; 9 | 10 | static size_t xallocs(void) { 11 | return _nallocs; 12 | } 13 | 14 | static void *xmalloc(size_t size) { 15 | void *ptr = malloc(size); 16 | assert(ptr); 17 | _nallocs++; 18 | return ptr; 19 | } 20 | 21 | static void xfree(void *ptr) { 22 | assert(ptr); 23 | free(ptr); 24 | _nallocs--; 25 | } 26 | 27 | // https://github.com/tidwall/th64 28 | static uint64_t th64(const void *data, size_t len, uint64_t seed) { 29 | uint8_t*p=(uint8_t*)data,*e=p+len; 30 | uint64_t r=0x14020a57acced8b7,x,h=seed; 31 | while(p+8<=e)memcpy(&x,p,8),x*=r,p+=8,x=x<<31|x>>33,h=h*r^x,h=h<<31|h>>33; 32 | while(p>31,h*=r,h^=h>>31,h*=r,h^=h>>31,h*=r,h); 34 | } 35 | 36 | static uint64_t strhash(const char *str) { 37 | return th64(str, strlen(str), 0); 38 | } 39 | 40 | int main(void) { 41 | int N = 1000000; 42 | struct ptx_graph_opts opts = { 43 | .malloc = xmalloc, 44 | .free = xfree, 45 | .autogc = -1, 46 | }; 47 | 48 | struct ptx_graph *graph = ptx_graph_new(&opts); 49 | 50 | struct ptx_node **txs = xmalloc(N * sizeof(struct ptx_node*)); 51 | 52 | size_t nallocs = xallocs(); 53 | 54 | struct ptx_node *T1 = 0, *T2 = 0, *T3 = 0, *T4 = 0, *T5 = 0; 55 | (void)T1;(void)T2;(void)T3;(void)T4;(void)T5; 56 | 57 | #define BEGIN(T) (T)=ptx_graph_begin(graph, 0);ptx_node_setlabel((T), #T); 58 | #define READ(T,K) ptx_node_read((T),strhash((K))) 59 | #define WRITE(T,K) ptx_node_write((T),strhash((K))) 60 | #define COMMIT(T) ptx_node_commit((T));(T)=0 61 | #define ROLLBACK(T) ptx_node_rollback((T));(T)=0 62 | #define TXDO(name, writeedges, func, expect) \ 63 | if (graph) { \ 64 | ptx_graph_gc(graph); \ 65 | if (xallocs() != nallocs) { \ 66 | printf("FAIL: (leaked memory) expected %zu allocations, got %zu\n",\ 67 | nallocs, xallocs()); \ 68 | exit(1); \ 69 | } \ 70 | ptx_graph_free(graph); \ 71 | } \ 72 | graph = ptx_graph_new(&opts); \ 73 | printf("========================\n");\ 74 | printf("===%*s%*s===\n",\ 75 | 9+(int)strlen(name)/2,name,9-(int)strlen(name)/2,"");\ 76 | printf("========================\n");\ 77 | func \ 78 | ptx_graph_print(graph, (writeedges));\ 79 | printf("\n"); \ 80 | { \ 81 | char statestr[65000]; \ 82 | ptx_graph_print_state(graph, statestr); \ 83 | if (strcmp(statestr, expect) != 0) { \ 84 | printf("FAIL: expected '%s', got '%s'\n", expect, statestr); \ 85 | exit(1); \ 86 | }; \ 87 | } 88 | 89 | TXDO("write-skew-2", 1, { 90 | BEGIN(T1); 91 | READ(T1, "doctors"); 92 | BEGIN(T2); 93 | READ(T2, "doctors"); 94 | WRITE(T1, "doctors"); 95 | COMMIT(T1); 96 | WRITE(T2, "doctors"); 97 | COMMIT(T2); 98 | }, "T1 COMMIT, T2 ROLLBACK"); 99 | 100 | TXDO("write-skew-3", 1, { 101 | BEGIN(T1); 102 | READ(T1, "doctors"); 103 | BEGIN(T2); 104 | READ(T2, "doctors"); 105 | BEGIN(T3); 106 | READ(T3, "doctors"); 107 | WRITE(T1, "doctors"); 108 | COMMIT(T1); 109 | WRITE(T2, "doctors"); 110 | COMMIT(T2); 111 | WRITE(T3, "doctors"); 112 | COMMIT(T3); 113 | }, "T1 COMMIT, T2 ROLLBACK, T3 ROLLBACK"); 114 | 115 | TXDO("write-skew-3-alt", 1, { 116 | BEGIN(T1); 117 | READ(T1, "doctors"); 118 | BEGIN(T2); 119 | READ(T2, "doctors"); 120 | WRITE(T1, "doctors"); 121 | COMMIT(T1); 122 | BEGIN(T3); 123 | READ(T3, "doctors"); 124 | WRITE(T2, "doctors"); 125 | COMMIT(T2); 126 | WRITE(T3, "doctors"); 127 | COMMIT(T3); 128 | }, "T1 COMMIT, T2 ROLLBACK, T3 ROLLBACK"); 129 | 130 | TXDO("receipts", 1, { 131 | BEGIN(T2); 132 | READ(T2, "current-batch"); 133 | BEGIN(T3); 134 | WRITE(T3, "current-batch"); 135 | COMMIT(T3); 136 | BEGIN(T1); 137 | READ(T1, "current-batch"); 138 | READ(T1, "receipts"); 139 | COMMIT(T1); 140 | WRITE(T2, "receipts"); 141 | COMMIT(T2); 142 | }, "T2 ROLLBACK, T3 COMMIT, T1 COMMIT"); 143 | 144 | TXDO("dots-2", 1, { 145 | BEGIN(T1); 146 | WRITE(T1, "dots"); 147 | BEGIN(T2); 148 | WRITE(T2, "dots"); 149 | COMMIT(T2); 150 | 151 | BEGIN(T2); 152 | READ(T2, "dots"); 153 | COMMIT(T2); 154 | COMMIT(T1); 155 | 156 | BEGIN(T1); 157 | WRITE(T1, "dots"); 158 | COMMIT(T1); 159 | }, "T1 ROLLBACK, T2 COMMIT, T2 COMMIT, T1 ROLLBACK"); 160 | 161 | TXDO("intersecting", 1, { 162 | BEGIN(T1); 163 | READ(T1, "mytab"); 164 | WRITE(T1, "mytab"); 165 | BEGIN(T2); 166 | READ(T2, "mytab"); 167 | WRITE(T2, "mytab"); 168 | COMMIT(T2); 169 | COMMIT(T1); 170 | }, "T1 ROLLBACK, T2 COMMIT"); 171 | 172 | TXDO("overdraft", 1, { 173 | BEGIN(T1); 174 | READ(T1, "checking"); 175 | READ(T1, "saving"); 176 | 177 | BEGIN(T2); 178 | READ(T2, "checking"); 179 | READ(T2, "saving"); 180 | WRITE(T1, "saving"); 181 | WRITE(T2, "checking"); 182 | COMMIT(T1); 183 | COMMIT(T2); 184 | }, "T1 COMMIT, T2 ROLLBACK"); 185 | 186 | TXDO("write-write", 1, { 187 | BEGIN(T1); 188 | WRITE(T1, "dots"); 189 | BEGIN(T2); 190 | WRITE(T2, "dots"); 191 | COMMIT(T1); 192 | COMMIT(T2); 193 | }, "T1 COMMIT, T2 ROLLBACK"); 194 | 195 | TXDO("write-read", 1, { 196 | BEGIN(T1); 197 | WRITE(T1, "dots"); 198 | BEGIN(T2); 199 | READ(T2, "dots"); 200 | COMMIT(T2); 201 | COMMIT(T1); 202 | }, "T1 COMMIT, T2 COMMIT"); 203 | 204 | xfree(txs); 205 | ptx_graph_free(graph); 206 | 207 | if (xallocs() != 0) { 208 | printf("%zu remaining allocations\n", xallocs()); 209 | printf("FAIL\n"); 210 | abort(); 211 | } 212 | 213 | printf("PASSED\n"); 214 | 215 | return 0; 216 | } 217 | --------------------------------------------------------------------------------