diff --git a/bdb/bdb_api.h b/bdb/bdb_api.h index b4b0f2aa13..1e565262d9 100644 --- a/bdb/bdb_api.h +++ b/bdb/bdb_api.h @@ -2080,6 +2080,7 @@ int bdb_get_myseqnum(bdb_state_type *bdb_state, seqnum_type *seqnum); void bdb_replace_handle(bdb_state_type *parent, int ix, bdb_state_type *handle); +int bdb_reset_lock_counters(bdb_state_type *bdb_state); int bdb_get_lock_counters(bdb_state_type *bdb_state, int64_t *deadlocks, int64_t *deadlock_locks, int64_t *waits, int64_t *requests); diff --git a/bdb/info.c b/bdb/info.c index 029264a942..510fb002e4 100644 --- a/bdb/info.c +++ b/bdb/info.c @@ -218,6 +218,14 @@ int bdb_get_lock_counters(bdb_state_type *bdb_state, int64_t *deadlocks, return 0; } +int bdb_reset_lock_counters(bdb_state_type *bdb_state) +{ + DB_LOCK_STAT *lock_stats = NULL; + int rc = bdb_state->dbenv->lock_stat(bdb_state->dbenv, &lock_stats, DB_STAT_CLEAR); + free(lock_stats); + return rc; +} + int bdb_get_bpool_counters(bdb_state_type *bdb_state, int64_t *bpool_hits, int64_t *bpool_misses, int64_t *rw_evicts) { diff --git a/berkdb/lock/lock.c b/berkdb/lock/lock.c index 37c349f54f..4fdac86c12 100644 --- a/berkdb/lock/lock.c +++ b/berkdb/lock/lock.c @@ -56,6 +56,8 @@ static const char revid[] = "$Id: lock.c,v 11.134 2003/11/18 21:30:38 ubell Exp +__thread int64_t bdb_thr_lockreqs = 0; + extern int verbose_deadlocks; extern int gbl_rowlocks; extern int gbl_page_latches; @@ -2241,6 +2243,7 @@ __lock_get_internal_int(lt, locker, in_locker, flags, obj, lock_mode, timeout, } region->stat.st_nrequests++; + bdb_thr_lockreqs++; sh_locker = *in_locker; diff --git a/db/comdb2.c b/db/comdb2.c index 9239cc6b78..4923e89f68 100644 --- a/db/comdb2.c +++ b/db/comdb2.c @@ -680,6 +680,13 @@ int gbl_requeue_on_tran_dispatch = 1; int gbl_crc32c = 1; int gbl_repscore = 0; int gbl_surprise = 1; // TODO: change name to something else +int gbl_partition_unique = 0; +int gbl_partition_unique_debug = 0; +int gbl_partition_unique_skip_locks = 0; +long long gbl_lockreqs_deadlocked = 0; +long long gbl_lockreqs_in_pdt = 0; +long long gbl_dka_calls = 0; +long long gbl_cross_shard_calls = 0; int gbl_check_wrong_db = 1; int gbl_broken_max_rec_sz = 0; int gbl_private_blkseq = 1; diff --git a/db/comdb2.h b/db/comdb2.h index 0b3ef2141f..9937ebc02a 100644 --- a/db/comdb2.h +++ b/db/comdb2.h @@ -1369,6 +1369,12 @@ struct ireq { char debug_buf[256]; char tzname[DB_MAX_TZNAMEDB]; + /* shard dbtables for cross-shard unique check (TRUNCATE partitions). + * Lives in region 2 (not zeroed by init_ireq_legacy's region3 bzero). + * Set by OSQL_PARTITION_SHARDS handler; freed at transaction end. */ + struct dbtable **partition_shards; + int npartition_shards; + /************/ /* REGION 3 */ /************/ @@ -1432,6 +1438,7 @@ struct ireq { uint8_t **idxInsert; uint8_t **idxDelete; + /* osql prefault step index */ int *osql_step_ix; @@ -3549,6 +3556,9 @@ extern int gbl_repscore; extern int gbl_max_verify_retries; extern int gbl_surprise; +extern int gbl_partition_unique; +extern int gbl_partition_unique_debug; +extern int gbl_partition_unique_skip_locks; extern int gbl_check_wrong_db; diff --git a/db/constraints.c b/db/constraints.c index 1a628bedc4..51999c000d 100644 --- a/db/constraints.c +++ b/db/constraints.c @@ -23,6 +23,7 @@ #include "cdb2_constants.h" #include "comdb2.h" +#include "comdb2_atomic.h" #include #include "tag.h" #include "tohex.h" @@ -243,6 +244,14 @@ static inline void free_cached_delayed_indexes(struct ireq *iq) free(iq->idxDelete); iq->idxInsert = iq->idxDelete = NULL; } + /* partition_shards must survive the entire transaction */ + if (gbl_partition_unique_debug && gbl_partition_unique) { + if (!iq->partition_shards) { + static __thread int free_nil_count = 0; + if (++free_nil_count <= 5) + logmsg(LOGMSG_USER, "free_cached_delayed_indexes: partition_shards already NULL! iq=%p\n", (void *)iq); + } + } } enum ct_etype { CTE_ADD = 1, CTE_DEL, CTE_UPD }; @@ -1171,6 +1180,10 @@ int upsert_collision_should_force_verify_error(int flags, int ixnum) int delayed_key_adds(struct ireq *iq, void *trans, int *blkpos, int *ixout, int *errout) { + if (gbl_partition_unique_debug) { + extern long long gbl_dka_calls; + ATOMIC_ADD64(gbl_dka_calls, 1); + } int rc = 0, fndlen = 0, err = 0, limit = 0, dup_txn_insert = 0; int idx = 0, ixkeylen = -1; void *od_dta = NULL; @@ -1408,9 +1421,26 @@ int delayed_key_adds(struct ireq *iq, void *trans, int *blkpos, int *ixout, /* light the prefault kill bit for this subop - newkeys */ prefault_kill_bits(iq, doidx, PFRQ_NEWKEY); + /* cross-shard unique check for TRUNCATE partitions */ + if (gbl_partition_unique_debug) { + static __thread int dka_count = 0; + if (++dka_count <= 3) + logmsg(LOGMSG_USER, "delayed_key_adds: iq=%p partition_shards=%p nshards=%d skip_locks=%d\n", + (void *)iq, (void *)iq->partition_shards, iq->npartition_shards, + gbl_partition_unique_skip_locks); + } + rc = check_cross_shard_unique(iq, trans, doidx, key, ixkeylen, errout); + if (rc == RC_INTERNAL_RETRY) { + *blkpos = curop->blkpos; + close_constraint_table_cursor(cur); + free_cached_delayed_indexes(iq); + ERR(rc, "deadlock", 0); + } + /* add the key */ - rc = ix_addk(iq, trans, key, doidx, genid, addrrn, od_dta_tail, - od_tail_len, ix_isnullk(iq->usedb, key, doidx)); + if (!rc) + rc = ix_addk(iq, trans, key, doidx, genid, addrrn, od_dta_tail, od_tail_len, + ix_isnullk(iq->usedb, key, doidx)); if (iq->vfy_idx_track) { dup_txn_insert = track_record_index(iq, doidx, key, ixkeylen); @@ -1431,11 +1461,7 @@ int delayed_key_adds(struct ireq *iq, void *trans, int *blkpos, int *ixout, *errout = OP_FAILED_VERIFY; rc = ERR_VERIFY; } else { - reqerrstr(iq, COMDB2_CSTRT_RC_DUP, - "add key constraint duplicate key '%s' on table " - "'%s' index %d", - get_keynm_from_db_idx(iq->usedb, doidx), - iq->usedb->tablename, doidx); + reqerrstr_dup_key(iq, iq->usedb, doidx); *errout = OP_FAILED_UNIQ; } diff --git a/db/db_metrics.c b/db/db_metrics.c index 1e94fb8f57..87bb4d7af9 100644 --- a/db/db_metrics.c +++ b/db/db_metrics.c @@ -47,6 +47,10 @@ struct comdb2_metrics_store { int64_t locks_aborted; int64_t fstraps; int64_t lockrequests; + int64_t lockreqs_deadlocked; + int64_t lockreqs_in_pdt; + int64_t dka_calls; + int64_t cross_shard_calls; int64_t lockwaits; int64_t lock_wait_time_us; int64_t memory_ulimit; @@ -202,6 +206,14 @@ comdb2_metric gbl_metrics[] = { &stats.ismaster, NULL}, {"lockrequests", "Total lock requests", STATISTIC_INTEGER, STATISTIC_COLLECTION_TYPE_CUMULATIVE, &stats.lockrequests, NULL}, + {"lockreqs_deadlocked", "Lock requests from deadlocked transaction attempts (requires partition_unique_debug)", + STATISTIC_INTEGER, STATISTIC_COLLECTION_TYPE_CUMULATIVE, &stats.lockreqs_deadlocked, NULL}, + {"lockreqs_in_pdt", "Lock requests from cross-shard unique checks (requires partition_unique_debug)", + STATISTIC_INTEGER, STATISTIC_COLLECTION_TYPE_CUMULATIVE, &stats.lockreqs_in_pdt, NULL}, + {"dka_calls", "Deferred key add invocations (requires partition_unique_debug)", STATISTIC_INTEGER, + STATISTIC_COLLECTION_TYPE_CUMULATIVE, &stats.dka_calls, NULL}, + {"cross_shard_calls", "Cross-shard unique index checks (requires partition_unique_debug)", STATISTIC_INTEGER, + STATISTIC_COLLECTION_TYPE_CUMULATIVE, &stats.cross_shard_calls, NULL}, {"lockwaits", "Number of lock waits", STATISTIC_INTEGER, STATISTIC_COLLECTION_TYPE_CUMULATIVE, &stats.lockwaits, NULL}, {"lockwait_time", "Time spent in lock waits (us)", STATISTIC_INTEGER, STATISTIC_COLLECTION_TYPE_CUMULATIVE, @@ -575,6 +587,15 @@ int refresh_metrics(void) stats.sql_ssl_count = gbl_nnewsql_ssl; stats.current_connections = net_get_num_current_non_appsock_accepts(thedb->handle_sibling) + active_appsock_conns; + extern long long gbl_lockreqs_deadlocked; + extern long long gbl_lockreqs_in_pdt; + extern long long gbl_dka_calls; + extern long long gbl_cross_shard_calls; + stats.lockreqs_deadlocked = gbl_lockreqs_deadlocked; + stats.lockreqs_in_pdt = gbl_lockreqs_in_pdt; + stats.dka_calls = gbl_dka_calls; + stats.cross_shard_calls = gbl_cross_shard_calls; + rc = bdb_get_lock_counters(thedb->bdb_env, &stats.deadlocks, &stats.locks_aborted, &stats.lockwaits, &stats.lockrequests); diff --git a/db/db_tunables.h b/db/db_tunables.h index 7b01c0f83e..96e8fe1ab4 100644 --- a/db/db_tunables.h +++ b/db/db_tunables.h @@ -2573,6 +2573,19 @@ REGISTER_TUNABLE("transaction_grace_period", "Time to wait for connections with pending transactions to go away on exit. (Default: 60)", TUNABLE_INTEGER, &gbl_transaction_grace_period, 0, NULL, NULL, NULL, NULL); +REGISTER_TUNABLE("partition_unique", + "Enforce unique index constraints across all shards of a TRUNCATE time partition (Default: off). " + "Does not validate pre-existing data; cross-shard duplicates present before enabling this tunable " + "are the user's responsibility to detect and resolve.", + TUNABLE_BOOLEAN, &gbl_partition_unique, NOARG, NULL, NULL, NULL, NULL); +REGISTER_TUNABLE("partition_unique_debug", + "Enable debug tracing for partition_unique cross-shard unique enforcement (Default: off). " + "Logs packet build/send on replicant, receive/store and free on master, and each cross-shard check.", + TUNABLE_BOOLEAN, &gbl_partition_unique_debug, NOARG, NULL, NULL, NULL, NULL); +REGISTER_TUNABLE("partition_unique_skip_locks", + "Debug: when set, OSQL_PARTITION_SHARDS will not acquire read table locks for sibling shards " + "(Default: off)", + TUNABLE_BOOLEAN, &gbl_partition_unique_skip_locks, NOARG, NULL, NULL, NULL, NULL); REGISTER_TUNABLE("partition_sc_reorder", "If the schema change is serialized for a partition, run current shard last", TUNABLE_BOOLEAN, &gbl_partition_sc_reorder, 0, NULL, NULL, NULL, NULL); REGISTER_TUNABLE("partition_retroactively", diff --git a/db/handle_buf.c b/db/handle_buf.c index 6471406773..cc6e8447ac 100644 --- a/db/handle_buf.c +++ b/db/handle_buf.c @@ -951,6 +951,13 @@ static int init_ireq_legacy(struct dbenv *dbenv, struct ireq *iq, COMDB2BUF *sb, iq->debug_buf[0] = '\0'; iq->tzname[0] = '\0'; + /* partition_shards lives in region 2 and must be explicitly initialized */ + if (gbl_partition_unique_debug && iq->partition_shards) + logmsg(LOGMSG_USER, "%s: [master] freeing partition_shards nshards=%d\n", __func__, iq->npartition_shards); + free(iq->partition_shards); + iq->partition_shards = NULL; + iq->npartition_shards = 0; + iq->where = "setup"; iq->frommach = frommach ? intern(frommach) : NULL; iq->frompid = frompid; diff --git a/db/indices.c b/db/indices.c index 6332f3dca9..71b1f5b575 100644 --- a/db/indices.c +++ b/db/indices.c @@ -16,6 +16,7 @@ #include #include +#include "comdb2_atomic.h" #include "schemachange.h" #include "block_internal.h" #include "logmsg.h" @@ -349,7 +350,6 @@ static inline void append_genid_to_key(dtikey_t *ditk, int ixkeylen) goto done; \ } while (0); - int add_record_indices(struct ireq *iq, void *trans, blob_buffer_t *blobs, size_t maxblobs, int *opfailcode, int *ixfailnum, int *rrn, unsigned long long *genid, @@ -440,6 +440,14 @@ int add_record_indices(struct ireq *iq, void *trans, blob_buffer_t *blobs, gbl_osqlpf_step[*(iq->osql_step_ix)].step += 2; if (reorder) { + /* cross-shard unique check before deferring the key */ + rc = check_cross_shard_unique(iq, trans, ixnum, key, ixkeylen, opfailcode); + if (rc == RC_INTERNAL_RETRY) { + ERR(rc, "deadlock", 0); + } else if (rc) { + *ixfailnum = ixnum; + ERR(rc, "partition unique constraint violation", 0); + } // if not datacopy, no need to save od_dta_tail void *data = NULL; int datalen = 0; @@ -488,6 +496,15 @@ int add_record_indices(struct ireq *iq, void *trans, blob_buffer_t *blobs, vgenid = 0; // no need to verify again } + /* cross-shard unique check for TRUNCATE partitions */ + rc = check_cross_shard_unique(iq, trans, ixnum, key, ixkeylen, opfailcode); + if (rc == RC_INTERNAL_RETRY) { + ERR(rc, "deadlock", 0); + } else if (rc) { + *ixfailnum = ixnum; + ERR(rc, "partition unique constraint violation", 0); + } + /* add the key */ rc = ix_addk(iq, trans, key, ixnum, *genid, *rrn, od_dta_tail, od_tail_len, isnullk); @@ -533,6 +550,88 @@ int add_record_indices(struct ireq *iq, void *trans, blob_buffer_t *blobs, return rc; } +/* + * Cross-shard unique check for TRUNCATE time partitions. + * Returns IX_DUP if the key exists in any sibling shard, RC_INTERNAL_RETRY on + * deadlock, 0 if no violation found. Sets *opfailcode on violation. + */ +int check_cross_shard_unique(struct ireq *iq, void *trans, int ixnum, char *key, int keylen, int *opfailcode) +{ + if (!gbl_partition_unique || !iq->partition_shards) { + if (gbl_partition_unique_debug && gbl_partition_unique && !iq->partition_shards) + logmsg(LOGMSG_USER, "check_cross_shard_unique: partition_shards is NULL for %s\n", + iq->usedb ? iq->usedb->tablename : "NULL"); + return 0; + } + if (iq->usedb->ix_dupes[ixnum] != 0) + return 0; + if (ix_isnullk(iq->usedb, key, ixnum)) + return 0; + + if (gbl_partition_unique_debug) { + extern long long gbl_cross_shard_calls; + ATOMIC_ADD64(gbl_cross_shard_calls, 1); + } + + extern __thread int64_t bdb_thr_lockreqs; + int64_t lr_start = gbl_partition_unique_debug ? bdb_thr_lockreqs : 0; + int rc = 0; + struct dbtable *saveddb = iq->usedb; + if (gbl_partition_unique_debug) + logmsg(LOGMSG_USER, "%s: [master] cross-shard check table='%s' ix=%d nshards=%d\n", __func__, + saveddb->tablename, ixnum, iq->npartition_shards); + for (int s = 0; s < iq->npartition_shards && !rc; s++) { + if (iq->partition_shards[s] == saveddb) + continue; + int fndrrn = 0; + unsigned long long fndgenid = 0; + iq->usedb = iq->partition_shards[s]; + int src = ix_find_by_key_tran(iq, key, keylen, ixnum, NULL, &fndrrn, &fndgenid, NULL, NULL, 0, trans); + if (gbl_partition_unique_debug) + logmsg(LOGMSG_USER, "%s: [master] checked shard='%s' ix=%d rc=%d\n", __func__, iq->usedb->tablename, + ixnum, src); + if (src == IX_FND) { + *opfailcode = OP_FAILED_UNIQ; + rc = IX_DUP; + } else if (src == RC_INTERNAL_RETRY) { + rc = RC_INTERNAL_RETRY; + } + } + iq->usedb = saveddb; + if (gbl_partition_unique_debug) { + extern long long gbl_lockreqs_in_pdt; + int64_t delta = bdb_thr_lockreqs - lr_start; + if (delta > 0) + ATOMIC_ADD64(gbl_lockreqs_in_pdt, delta); + } + return rc; +} + +void reqerrstr_dup_key(struct ireq *iq, struct dbtable *db, int ixnum) +{ + if (gbl_partition_unique && db->timepartition_name) + reqerrstr(iq, COMDB2_CSTRT_RC_DUP, + "add key constraint duplicate key '%s' on partition '%s' shard '%s' index %d", + get_keynm_from_db_idx(db, ixnum), db->timepartition_name, db->tablename, ixnum); + else + reqerrstr(iq, COMDB2_CSTRT_RC_DUP, "add key constraint duplicate key '%s' on table '%s' index %d", + get_keynm_from_db_idx(db, ixnum), db->tablename, ixnum); +} + +void reqerrstr_uncommittable_dup(struct ireq *iq, struct dbtable *db, int ixnum) +{ + if (gbl_partition_unique && db->timepartition_name) + reqerrstr(iq, COMDB2_CSTRT_RC_DUP, + "Transaction is uncommittable: Duplicate insert on key '%s' " + "on partition '%s' shard '%s' index %d", + get_keynm_from_db_idx(db, ixnum), db->timepartition_name, db->tablename, ixnum); + else + reqerrstr(iq, COMDB2_CSTRT_RC_DUP, + "Transaction is uncommittable: Duplicate insert on key '%s' " + "in table '%s' index %d", + get_keynm_from_db_idx(db, ixnum), db->tablename, ixnum); +} + /* * Add an individual key. The operation * is defered until the end of the block op (we call insert_add_op). @@ -560,8 +659,19 @@ static int add_key(struct ireq *iq, void *trans, int ixnum, return ERR_INTERNAL; } - rc = ix_addk(iq, trans, newkey, ixnum, genid, rrn, od_dta_tail, - od_tail_len, ix_isnullk(iq->usedb, newkey, ixnum)); + /* cross-shard unique check for TRUNCATE partitions. + * NOTE: this check is currently a no-op for the cascade path. + * ON UPDATE CASCADE is handled entirely on the master side in + * constraints.c without going through the OSQL stream, so + * iq->partition_shards is never populated here and + * check_cross_shard_unique returns 0 immediately. + * This should become effective once cascading on time partitions + * is implemented and the master cascade processor populates + * iq->partition_shards before invoking upd_record_indices. */ + rc = check_cross_shard_unique(iq, trans, ixnum, newkey, getkeysize(iq->usedb, ixnum), opfailcode); + if (!rc) + rc = ix_addk(iq, trans, newkey, ixnum, genid, rrn, od_dta_tail, od_tail_len, + ix_isnullk(iq->usedb, newkey, ixnum)); if (iq->debug) { reqprintf(iq, "ix_addk IX %d RRN %d KEY ", ixnum, rrn); reqdumphex(iq, newkey, getkeysize(iq->usedb, ixnum)); @@ -569,7 +679,7 @@ static int add_key(struct ireq *iq, void *trans, int ixnum, } if (rc == IX_DUP) *opfailcode = OP_FAILED_UNIQ; - else if (rc != 0) + else if (rc != 0 && rc != RC_INTERNAL_RETRY) *opfailcode = OP_FAILED_INTERNAL; } @@ -1371,6 +1481,19 @@ int process_defered_table(struct ireq *iq, void *trans, int *blkpos, int *ixout, if (ditk->type == DIT_ADD) { int addrrn = 2; + /* cross-shard unique check for TRUNCATE partitions */ + int opfailcode_tmp = 0; + rc = check_cross_shard_unique(iq, trans, ditk->ixnum, ditk->ixkey, getkeysize(iq->usedb, ditk->ixnum), + &opfailcode_tmp); + if (rc == IX_DUP) { + reqerrstr_dup_key(iq, ditk->usedb, ditk->ixnum); + *errout = OP_FAILED_UNIQ; + *ixout = ditk->ixnum; + goto done; + } else if (rc == RC_INTERNAL_RETRY) { + *errout = OP_FAILED_INTERNAL; + goto done; + } /* add the key */ rc = ix_addk(iq, trans, ditk->ixkey, ditk->ixnum, ditk->genid, addrrn, od_dta_tail, od_tail_len, @@ -1385,15 +1508,7 @@ int process_defered_table(struct ireq *iq, void *trans, int *blkpos, int *ixout, } if (rc == IX_DUP) { - reqerrstr(iq, COMDB2_CSTRT_RC_DUP, - "add key constraint " - "duplicate key '%s' on " - "table '%s' index %d", - get_keynm_from_db_idx(ditk->usedb, ditk->ixnum), - ditk->usedb->tablename, ditk->ixnum); - - //*blkpos = curop->blkpos; - + reqerrstr_dup_key(iq, ditk->usedb, ditk->ixnum); *errout = OP_FAILED_UNIQ; *ixout = ditk->ixnum; goto done; diff --git a/db/indices.h b/db/indices.h index 08398b6a68..355473cae0 100644 --- a/db/indices.h +++ b/db/indices.h @@ -59,4 +59,9 @@ int del_new_record_indices(struct ireq *iq, void *trans, unsigned long long del_keys, blob_buffer_t *del_idx_blobs, int verify_retry); +int check_cross_shard_unique(struct ireq *iq, void *trans, int ixnum, char *key, int keylen, int *opfailcode); + +void reqerrstr_dup_key(struct ireq *iq, struct dbtable *db, int ixnum); +void reqerrstr_uncommittable_dup(struct ireq *iq, struct dbtable *db, int ixnum); + #endif diff --git a/db/osqlblockproc.c b/db/osqlblockproc.c index 44724684fa..325fbacbbf 100644 --- a/db/osqlblockproc.c +++ b/db/osqlblockproc.c @@ -506,6 +506,7 @@ static int setup_reorder_key(blocksql_tran_t *tran, int type, osql_sess_t *sess, break; } /* This doesn't touch btrees and should be processed first */ + case OSQL_PARTITION_SHARDS: case OSQL_SERIAL: case OSQL_SELECTV: key->tbl_idx = 0; @@ -518,6 +519,7 @@ static int setup_reorder_key(blocksql_tran_t *tran, int type, osql_sess_t *sess, } switch (type) { + case OSQL_PARTITION_SHARDS: case OSQL_QBLOB: case OSQL_DELIDX: case OSQL_INSIDX: diff --git a/db/osqlcomm.c b/db/osqlcomm.c index d7163f90c7..b92a26f49c 100644 --- a/db/osqlcomm.c +++ b/db/osqlcomm.c @@ -58,6 +58,7 @@ #include "sc_logic.h" #include "eventlog.h" #include +#include "indices.h" #define MAX_CLUSTER REPMAX @@ -4123,6 +4124,60 @@ int osql_send_usedb(osql_target_t *target, unsigned long long rqid, uuid_t uuid, return rc; } +/** + * Send OSQL_PARTITION_SHARDS op + * Informs the master of all shard table names belonging to a TRUNCATE + * partition so it can lock them before any writes arrive. + * Payload: 4-byte big-endian shard count followed by nshards null-terminated names. + */ +int osql_send_partition_shards(osql_target_t *target, unsigned long long rqid, uuid_t uuid, char **shards, int nshards, + int type) +{ + if (check_master(target)) + return OSQL_SEND_ERROR_WRONGMASTER; + + /* compute total payload size: 4 bytes for count + names */ + int payloadsz = sizeof(int); + for (int i = 0; i < nshards; i++) + payloadsz += strlen(shards[i]) + 1; + + char *payload = malloc(payloadsz); + if (!payload) + return -1; + + char *p = payload; + int nshards_wire = htonl(nshards); + memcpy(p, &nshards_wire, sizeof(int)); + p += sizeof(int); + for (int i = 0; i < nshards; i++) { + int len = strlen(shards[i]) + 1; + memcpy(p, shards[i], len); + p += len; + } + + uint8_t buf[OSQLCOMM_UUID_RPL_TYPE_LEN]; + uint8_t *p_buf = buf; + uint8_t *p_buf_end = buf + sizeof(buf); + if (rqid == OSQL_RQID_USE_UUID) { + osql_uuid_rpl_t hdr = {0}; + hdr.type = OSQL_PARTITION_SHARDS; + comdb2uuidcpy(hdr.uuid, uuid); + type = osql_net_type_to_net_uuid_type(NET_OSQL_SOCK_RPL); + osqlcomm_uuid_rpl_type_put(&hdr, p_buf, p_buf_end); + } else { + osql_rpl_t hdr_rqid = {0}; + hdr_rqid.type = OSQL_PARTITION_SHARDS; + hdr_rqid.sid = rqid; + osqlcomm_rpl_type_put(&hdr_rqid, p_buf, p_buf_end); + } + + int rc = target->send(target, type, buf, sizeof(buf), 0, payload, payloadsz); + free(payload); + if (rc) + logmsg(LOGMSG_ERROR, "%s target->send returns rc=%d\n", __func__, rc); + return rc; +} + /** * Send UPDCOLS op * It handles remote/local connectivity @@ -7326,11 +7381,7 @@ int osql_process_packet(struct ireq *iq, uuid_t uuid, void *trans, char **pmsg, if (err->errcode == OP_FAILED_UNIQ) { if (iq->vfy_idx_track == 1 && iq->dup_key_insert == 1) { rc = ERR_UNCOMMITTABLE_TXN; - reqerrstr(iq, COMDB2_CSTRT_RC_DUP, "Transaction is uncommittable: " - "Duplicate insert on key '%s' " - "in table '%s' index %d", - get_keynm_from_db_idx(iq->usedb, err->ixnum), - iq->usedb->tablename, err->ixnum); + reqerrstr_uncommittable_dup(iq, iq->usedb, err->ixnum); err->errcode = ERR_UNCOMMITTABLE_TXN; goto done_delete; } @@ -7361,11 +7412,7 @@ int osql_process_packet(struct ireq *iq, uuid_t uuid, void *trans, char **pmsg, if (rc != ERR_VERIFY) { /* this can happen if we're skipping delayed key adds */ - reqerrstr(iq, COMDB2_CSTRT_RC_DUP, "add key constraint " - "duplicate key '%s' on " - "table '%s' index %d", - get_keynm_from_db_idx(iq->usedb, err->ixnum), - iq->usedb->tablename, err->ixnum); + reqerrstr_dup_key(iq, iq->usedb, err->ixnum); } } else if (rc != RC_INTERNAL_RETRY) { errstat_cat_strf(&iq->errstat, " unable to add record rc = %d", @@ -7709,6 +7756,73 @@ int osql_process_packet(struct ireq *iq, uuid_t uuid, void *trans, char **pmsg, ); memcpy(pIdx, pData, dt.nData); } break; + case OSQL_PARTITION_SHARDS: { + /* Payload: 4-byte shard count followed by null-terminated shard names. + * Lock each shard table and store its dbtable pointer for use by + * add_record_indices() during cross-shard unique checks. */ + const uint8_t *local_buf_end = (const uint8_t *)msg + msglen; + if (local_buf_end - p_buf < (ptrdiff_t)sizeof(int)) { + logmsg(LOGMSG_ERROR, "%s: OSQL_PARTITION_SHARDS truncated\n", __func__); + return ERR_INTERNAL; + } + int nshards; + memcpy(&nshards, p_buf, sizeof(int)); + nshards = ntohl(nshards); + if (nshards < 0 || nshards > 1024) { + logmsg(LOGMSG_ERROR, "%s: OSQL_PARTITION_SHARDS bad nshards=%d\n", __func__, nshards); + return ERR_INTERNAL; + } + const char *name = (const char *)p_buf + sizeof(int); + const char *payload_end = (const char *)local_buf_end; + /* free previous partition shard list if any */ + free(iq->partition_shards); + iq->partition_shards = NULL; + iq->npartition_shards = 0; + + iq->partition_shards = malloc(nshards * sizeof(struct dbtable *)); + if (!iq->partition_shards) + return ERR_INTERNAL; + + for (int i = 0; i < nshards; i++) { + /* ensure there is at least one byte and a NUL terminator in bounds */ + const char *nul = memchr(name, '\0', payload_end - name); + if (!nul) { + logmsg(LOGMSG_ERROR, "%s: OSQL_PARTITION_SHARDS payload truncated at shard %d\n", __func__, i); + free(iq->partition_shards); + iq->partition_shards = NULL; + iq->npartition_shards = 0; + return ERR_INTERNAL; + } + if (!gbl_partition_unique_skip_locks) { + rc = bdb_lock_tablename_read(thedb->bdb_env, name, trans); + if (rc == BDBERR_DEADLOCK) { + if (iq->debug) + reqprintf(iq, "LOCK PARTITION SHARD READ DEADLOCK"); + free(iq->partition_shards); + iq->partition_shards = NULL; + iq->npartition_shards = 0; + return RC_INTERNAL_RETRY; + } else if (rc) { + if (iq->debug) + reqprintf(iq, "LOCK PARTITION SHARD READ ERROR: %d", rc); + free(iq->partition_shards); + iq->partition_shards = NULL; + iq->npartition_shards = 0; + return ERR_INTERNAL; + } + } + iq->partition_shards[iq->npartition_shards] = get_dbtable_by_name(name); + if (iq->partition_shards[iq->npartition_shards]) + iq->npartition_shards++; + name = nul + 1; + } + if (gbl_partition_unique_debug) { + logmsg(LOGMSG_USER, "%s: [master] received OSQL_PARTITION_SHARDS nshards=%d stored=%d\n", __func__, + nshards, iq->npartition_shards); + for (int i = 0; i < iq->npartition_shards; i++) + logmsg(LOGMSG_USER, "%s: [master] shard[%d]='%s'\n", __func__, i, iq->partition_shards[i]->tablename); + } + } break; case OSQL_QBLOB: { osql_qblob_t dt = {0}; const uint8_t *p_buf_end = p_buf + sizeof(osql_qblob_t), diff --git a/db/osqlcomm.h b/db/osqlcomm.h index b5ea4c8e2a..e0e2ca06aa 100644 --- a/db/osqlcomm.h +++ b/db/osqlcomm.h @@ -60,6 +60,15 @@ int osql_comm_send_socksqlreq(osql_target_t *target, const char *sql, int sqlen, int osql_send_usedb(osql_target_t *target, unsigned long long rqid, uuid_t uuid, char *tablename, int type, unsigned long long version); +/** + * Send OSQL_PARTITION_SHARDS op + * Sends the list of shard table names for a TRUNCATE partition so the master + * can lock them before any writes arrive. + * + */ +int osql_send_partition_shards(osql_target_t *target, unsigned long long rqid, uuid_t uuid, char **shards, int nshards, + int type); + /** * Send INDEX op * It handles remote/local connectivity diff --git a/db/osqlrpltypes.h b/db/osqlrpltypes.h index 04148179ab..413a059405 100644 --- a/db/osqlrpltypes.h +++ b/db/osqlrpltypes.h @@ -54,7 +54,8 @@ XMACRO_OSQL_RPL_TYPES( OSQL_DONE_WITH_EFFECTS, 28, "OSQL_DONE_WITH_EFFECTS" ) XMACRO_OSQL_RPL_TYPES( OSQL_PREPARE, 29, "OSQL_PREPARE" ) /* participant should prepare */ \ XMACRO_OSQL_RPL_TYPES( OSQL_DIST_TXNID, 30, "OSQL_DIST_TXNID" ) /* send dist-txnid to coordinator */ \ XMACRO_OSQL_RPL_TYPES( OSQL_PARTICIPANT, 31, "OSQL_PARTICIPANT" ) /* a participant (to coordinator) */ \ -XMACRO_OSQL_RPL_TYPES( MAX_OSQL_TYPES, 32, "OSQL_MAX") +XMACRO_OSQL_RPL_TYPES( OSQL_PARTITION_SHARDS, 32, "OSQL_PARTITION_SHARDS" ) /* shard table list for a TRUNCATE partition */ \ +XMACRO_OSQL_RPL_TYPES( MAX_OSQL_TYPES, 33, "OSQL_MAX") // clang-format on diff --git a/db/osqlsqlthr.c b/db/osqlsqlthr.c index 0eda3a8475..9b01c0ebc1 100644 --- a/db/osqlsqlthr.c +++ b/db/osqlsqlthr.c @@ -799,6 +799,12 @@ int osql_updrec(struct BtCursor *pCur, struct sql_thread *thd, char *pData, if ((rc = check_osql_capacity(thd))) return rc; + if (gbl_partition_unique_debug) { + static __thread int updrec_count = 0; + if (++updrec_count <= 3) + logmsg(LOGMSG_USER, "osql_updrec: mode=%d tbl=%s skip_locks=%d\n", clnt->dbtran.mode, + pCur->db ? pCur->db->tablename : "NULL", gbl_partition_unique_skip_locks); + } if (clnt->dbtran.mode == TRANLEVEL_SOSQL) { START_SOCKSQL; do { @@ -943,6 +949,10 @@ static int osql_sock_restart(struct sqlclntstate *clnt, int maxretries, int keep osql->tablename = NULL; osql->tablenamelen = 0; } + if (osql->partition_name) { + free(osql->partition_name); + osql->partition_name = NULL; + } if (!keep_session) { if (gbl_master_swing_osql_verbose) @@ -1414,6 +1424,10 @@ int osql_sock_abort(struct sqlclntstate *clnt, int type) clnt->osql.tablename = NULL; clnt->osql.tablenamelen = 0; } + if (clnt->osql.partition_name) { + free(clnt->osql.partition_name); + clnt->osql.partition_name = NULL; + } if (gbl_debug_disttxn_trace) { uuidstr_t us; @@ -1435,6 +1449,10 @@ static int osql_send_usedb_logic_int(char *tablename, struct sqlclntstate *clnt, int rc = 0; int restarted; + if (gbl_partition_unique_debug) + logmsg(LOGMSG_USER, "osql_send_usedb_logic_int: tbl=%s cached=%s\n", tablename, + osql->tablename ? osql->tablename : "NULL"); + if (gbl_reject_mixed_ddl_dml && osql->running_ddl) { return SQLITE_DDL_MISUSE; } @@ -1473,6 +1491,56 @@ static int osql_send_usedb_logic_int(char *tablename, struct sqlclntstate *clnt, osql->replicant_numops++; DEBUG_PRINT_NUMOPS(); + if (rc != SQLITE_OK) + return rc; + + /* If this table is a shard of a TRUNCATE partition with unique indexes, + * send the full shard list so the master can lock all sibling shards. + * Deduplicate by partition name — only send once per partition per txn. */ + if (gbl_partition_unique) { + struct dbtable *db = get_dbtable_by_name(tablename); + const char *partname = db ? db->timepartition_name : NULL; + if (gbl_partition_unique_debug) + logmsg(LOGMSG_USER, "osql_send_usedb: partition_unique=1 tbl=%s partname=%s\n", tablename, + partname ? partname : "NULL"); + if (partname && (!osql->partition_name || strcmp(osql->partition_name, partname) != 0)) { + int nshards = 0; + struct errstat shard_err = {0}; + char **shards = timepart_get_shard_names(partname, &nshards, &shard_err); + if (!shards && shard_err.errval != 0) { + logmsg(LOGMSG_ERROR, "%s: %s\n", __func__, shard_err.errstr); + return SQLITE_NOMEM; + } + if (!shards) + logmsg(LOGMSG_USER, + "%s: [replicant] timepart_get_shard_names returned NULL for partition='%s' (no error)\n", + __func__, partname); + if (shards) { + if (gbl_partition_unique_debug) { + logmsg(LOGMSG_USER, "%s: [replicant] sending OSQL_PARTITION_SHARDS partition='%s' nshards=%d\n", + __func__, partname, nshards); + for (int i = 0; i < nshards; i++) + logmsg(LOGMSG_USER, "%s: [replicant] shard[%d]='%s'\n", __func__, i, shards[i]); + } + do { + rc = osql_send_partition_shards(&osql->target, osql->rqid, osql->uuid, shards, nshards, nettype); + RESTART_SOCKSQL; + } while (restarted); + + for (int i = 0; i < nshards; i++) + free(shards[i]); + free(shards); + + if (rc == SQLITE_OK) { + free(osql->partition_name); + osql->partition_name = strdup(partname); + } + osql->replicant_numops++; + DEBUG_PRINT_NUMOPS(); + } + } + } + return rc; } @@ -1608,6 +1676,10 @@ static int osql_send_commit_logic(struct sqlclntstate *clnt, int retries, int ne osql->tablename = NULL; osql->tablenamelen = 0; } + if (osql->partition_name) { + free(osql->partition_name); + osql->partition_name = NULL; + } osql->tran_ops = 0; /* reset transaction size counter*/ if (!clnt->dbtran.trans_has_sp) { diff --git a/db/process_message.c b/db/process_message.c index 4ae9940ad0..c020015f82 100644 --- a/db/process_message.c +++ b/db/process_message.c @@ -5182,6 +5182,17 @@ int process_command(struct dbenv *dbenv, char *line, int lline, int st) bdb_del_seqno(NULL); } else if (tokcmp(tok, ltok, "clear_sc_history") == 0) { bdb_clear_sc_history(); + } else if (tokcmp(tok, ltok, "clear_lock_stats") == 0) { + extern long long gbl_lockreqs_deadlocked; + extern long long gbl_lockreqs_in_pdt; + extern long long gbl_dka_calls; + extern long long gbl_cross_shard_calls; + bdb_reset_lock_counters(thedb->bdb_env); + gbl_lockreqs_deadlocked = 0; + gbl_lockreqs_in_pdt = 0; + gbl_dka_calls = 0; + gbl_cross_shard_calls = 0; + logmsg(LOGMSG_USER, "lock stats cleared\n"); } else { // see if any plugins know how to handle this struct message_handler *h; diff --git a/db/sql.h b/db/sql.h index c4e04c306a..a034839b76 100644 --- a/db/sql.h +++ b/db/sql.h @@ -144,6 +144,7 @@ typedef struct osqlstate { uuid_t uuid; /* session id, take 2 */ char *tablename; /* malloc-ed cache of send tablename for usedb */ int tablenamelen; /* tablename length */ + char *partition_name; /* malloc-ed cache of last sent partition name for OSQL_PARTITION_SHARDS */ int sentops; /* number of operations per statement */ int tran_ops; /* actual number of operations for a transaction */ int replicant_numops; /* total num of ops sent by replicant to master which diff --git a/db/sqlglue.c b/db/sqlglue.c index b8005c9965..93b8cf77da 100644 --- a/db/sqlglue.c +++ b/db/sqlglue.c @@ -8927,6 +8927,16 @@ int sqlite3BtreeInsert( struct sql_thread *thd = pCur->thd; struct sqlclntstate *clnt = pCur->clnt; + if (gbl_partition_unique_debug && pCur->db && pCur->db->timepartition_name) { + static int btinsert_logged = 0; + if (!btinsert_logged) { + logmsg(LOGMSG_USER, "sqlite3BtreeInsert: tbl=%s timepartition=%s is_remote=%d mode=%d\n", + pCur->db->tablename, pCur->db->timepartition_name, pCur->bt ? pCur->bt->is_remote : -1, + clnt ? clnt->dbtran.mode : -1); + btinsert_logged = 1; + } + } + pKey = pPayload->pKey; nKey = pPayload->nKey; pData = pPayload->pData; @@ -9193,6 +9203,14 @@ int sqlite3BtreeInsert( } } + if ((rec_flags & OSQL_IGNORE_FAILURE) && gbl_partition_unique && pCur->db->timepartition_name) { + sqlite3VdbeError(pCur->vdbe, + "UPSERT is not supported on time partition '%s' " + "when partition_unique is enabled", + pCur->db->timepartition_name); + return SQLITE_ERROR; + } + if (is_update) { /* Updating an existing record. */ rc = osql_updrec(pCur, thd, pCur->ondisk_buf, getdatsize(pCur->db), pCur->vdbe->updCols, pblobs, MAXBLOBS, rec_flags); diff --git a/db/toblock.c b/db/toblock.c index 6d36a09c94..886fdea245 100644 --- a/db/toblock.c +++ b/db/toblock.c @@ -80,6 +80,7 @@ #include "schemachange.h" #include "views.h" #include +#include "indices.h" #if 0 #define TEST_OSQL @@ -1898,9 +1899,7 @@ int tolongblock(struct ireq *iq) poll(0, 0, (rand() % 25 + 1)); goto retrysingle; } else { - logmsg(LOGMSG_ERROR, - "*ERROR* [%d] tolongblock too much contention %d\n", - __LINE__, retries); + logmsg(LOGMSG_ERROR, "*ERROR* [%d] tolongblock too much contention %d\n", __LINE__, retries); thd_dump(); } } @@ -2307,6 +2306,8 @@ static int osql_destroy_transaction(struct ireq *iq, tran_type **parent_trans, * and cleaned up. Note that this is inside of the retry loop. */ static int toblock_outer(struct ireq *iq, block_state_t *blkstate) { + extern __thread int64_t bdb_thr_lockreqs; + int64_t lr_start = gbl_partition_unique_debug ? bdb_thr_lockreqs : 0; int rc; int gaveaway; int i = 0; @@ -2473,6 +2474,10 @@ static int toblock_outer(struct ireq *iq, block_state_t *blkstate) } javasp_trans_end(iq->jsph); + if (gbl_partition_unique_debug && rc == RC_INTERNAL_RETRY) { + extern long long gbl_lockreqs_deadlocked; + ATOMIC_ADD64(gbl_lockreqs_deadlocked, (bdb_thr_lockreqs - lr_start)); + } return rc; } @@ -4938,6 +4943,14 @@ static int toblock_main_int(struct javasp_trans_state *javasp_trans_handle, stru ixout = -1; errout = 0; + if (gbl_partition_unique_debug) { + static __thread int tbk_count = 0; + if (++tbk_count <= 3) + logmsg(LOGMSG_USER, "toblock: delayed=%d reorder=%d osql_flags=0x%x partition_shards=%p nshards=%d\n", + delayed, osql_is_index_reorder_on(iq->osql_flags), iq->osql_flags, (void *)iq->partition_shards, + iq->npartition_shards); + } + if (delayed || gbl_goslow || osql_is_index_reorder_on(iq->osql_flags)) { if (osql_is_index_reorder_on(iq->osql_flags)) { @@ -4971,11 +4984,7 @@ static int toblock_main_int(struct javasp_trans_state *javasp_trans_handle, stru if (iq->vfy_idx_track == 1 && iq->dup_key_insert == 1) { rc = ERR_UNCOMMITTABLE_TXN; errout = ERR_UNCOMMITTABLE_TXN; - reqerrstr(iq, COMDB2_CSTRT_RC_DUP, "Transaction is uncommittable: " - "Duplicate insert on key '%s' " - "in table '%s' index %d", - get_keynm_from_db_idx(iq->usedb, ixout), - iq->usedb->tablename, ixout); + reqerrstr_uncommittable_dup(iq, iq->usedb, ixout); } else { check_serializability = 1; } diff --git a/db/views.c b/db/views.c index 6ae3961cf2..5095bee7ce 100644 --- a/db/views.c +++ b/db/views.c @@ -3613,6 +3613,38 @@ struct dbtable *timepart_retro_route(struct timepart_retro *retros, unsigned lon return ret; } +/** + * Return a heap-allocated array of strdup'd shard table names for a TRUNCATE + * partition. *nshards is set to the number of entries. Returns NULL if the + * partition does not exist or is not a TRUNCATE partition. On allocation + * failure, returns NULL and sets err. The caller must free each string and + * then the array itself. + * + */ +char **timepart_get_shard_names(const char *partname, int *nshards, struct errstat *err) +{ + *nshards = 0; + + views_lock(); + timepart_view_t *view = _get_view(thedb->timepart_views, partname); + if (!view || view->rolltype != TIMEPART_ROLLOUT_TRUNCATE) { + views_unlock(); + return NULL; + } + + char **shards = malloc(view->nshards * sizeof(char *)); + if (!shards) { + errstat_set_rcstrf(err, VIEW_ERR_MALLOC, "out of memory getting shard names for partition '%s'", partname); + views_unlock(); + return NULL; + } + for (int i = 0; i < view->nshards; i++) + shards[(*nshards)++] = strdup(view->shards[i].tblname); + views_unlock(); + + return shards; +} + #include "views_systable.c" #include "views_serial.c" diff --git a/db/views.h b/db/views.h index ccf6b4d507..f3fcbb4512 100644 --- a/db/views.h +++ b/db/views.h @@ -562,4 +562,13 @@ int timepart_analyze_partition(char *name, void *td, struct sqlclntstate *clnt, */ struct dbtable *timepart_retro_route(struct timepart_retro *retros, unsigned long long genid, const char *f, int l); +/** + * Copy the table names of all shards belonging to a TRUNCATE partition into + * the caller-supplied array. Returns the number of shards written (0 if the + * partition does not exist or is not a TRUNCATE partition). The caller must + * supply an array of at least MAXTABLELEN+1 bytes per slot. + * + */ +char **timepart_get_shard_names(const char *partname, int *nshards, struct errstat *err); + #endif diff --git a/docs/pages/programming/timepart.md b/docs/pages/programming/timepart.md index b6614136c6..7a82a6ee69 100644 --- a/docs/pages/programming/timepart.md +++ b/docs/pages/programming/timepart.md @@ -48,8 +48,8 @@ If the client would choose periodicity `daily`, and retention 31, at all time th ## Current limitations * The name space for tables and partitions is the same. Creating a partition name cannot reuse an existing table name. This is inconvenient and it will be addressed by future efforts. -* Enforcing unique constraints is not possible across shards of a time partition. An option to allow this at the expense of commit performance will be goal of a future project. -* UPSERT is not supported +* Cross-shard unique constraint enforcement is available for TRUNCATE partitions via the `partition_unique` tunable. When enabled, every unique index is enforced across all shards at the cost of additional index lookups per write. Note that enabling this tunable does not validate pre-existing data: any cross-shard duplicates present before the tunable is turned on will remain and are the user's responsibility to detect and resolve. +* UPSERT is not supported on time partitions. ## Rollout implementation details diff --git a/tests/timepart_ins_perf.test/Makefile b/tests/timepart_ins_perf.test/Makefile new file mode 100644 index 0000000000..d690074bc3 --- /dev/null +++ b/tests/timepart_ins_perf.test/Makefile @@ -0,0 +1,9 @@ +ifeq ($(TESTSROOTDIR),) + include ../testcase.mk +else + include $(TESTSROOTDIR)/testcase.mk +endif + +ifeq ($(TEST_TIMEOUT),) + export TEST_TIMEOUT=10m +endif diff --git a/tests/timepart_ins_perf.test/README b/tests/timepart_ins_perf.test/README new file mode 100644 index 0000000000..f6bd6e5c5a --- /dev/null +++ b/tests/timepart_ins_perf.test/README @@ -0,0 +1,71 @@ +timepart_ins_perf - Performance benchmark for TRUNCATE time partition unique checks + +Overview +-------- +Measures the overhead of cross-shard unique index enforcement on TRUNCATE +time partitions across three write workloads. Runs each workload under three +configurations (off / on / on-nolocks) and reports timing, deadlocks, and +lock stats per configuration. + + Phase 1 — INSERT: workers insert new rows into the partition. + Cross-shard unique checks fire for every unique index. + This phase shows the full overhead of the feature. + + Phase 2 — UPDATE non-key column: workers update column c (not unique). + Cross-shard checks do not fire. Serves as a control to confirm + the feature adds no overhead to non-unique writes. + + Phase 3 — DELETE: workers delete rows. + Cross-shard checks do not fire. Serves as a control. + +Each pass repopulates the partition to an identical initial state so all +three configurations (off / on / on-nolocks) execute the same workload. +Off and on pass order is randomised per iteration to neutralise cache effects. + +Parameters +---------- +Override on the make command line, e.g.: + make timepart_ins_perf ROWS_PER_SHARD=1000 NWORKERS=4 ITERATIONS=5 + + ROWS_PER_SHARD Rows per shard for background seed data and benchmark + (default: 10000) + NRANGES Number of non-overlapping row ranges / max workers (default: 8) + NWORKERS Parallel writers (default: NRANGES) + ITERATIONS Benchmark repetitions per phase (default: 3) + TXN_SIZE Rows per transaction (default: 10) + D_SIZE Bytes for randomblob() filler column (default: 16) + DEBUG_TRACE 1=enable partition_unique_debug tunable and collect detailed + per-counter stats; 0=skip debug counters (default: 1) + +Running +------- +Single node: + cd tests && make timepart_ins_perf + +With custom parameters: + make timepart_ins_perf ROWS_PER_SHARD=100 NWORKERS=2 ITERATIONS=1 + +Docker cluster (3 nodes): + cd tests/docker + ./runit -s -n 3 -f + + where run_script contains: + make CLUSTER="m1 m2 m3" SKIP_COPY_EXE=1 TESTDIR=$TESTDIR timepart_ins_perf + +Output +------ +Per-iteration timing line (one per phase): + iter 0 (off->on): off=750ms on=1480ms on-nolocks=1460ms + +Summary table per phase with totals, per-row averages, and % change vs off: + + off on on vs off on-nolocks nolocks vs off + total time (ms) 2261 4457 +97% 4398 +94% + ... + cross-shard lock requests 0 2106930 n/a 2105460 n/a + cross-shard unique checks 0 60198 n/a 60156 n/a + +When DEBUG_TRACE=1, additional rows appear (lock reqs deadlocked txns, +cross-shard lock requests, delayed key add calls, cross-shard unique checks). + +Timeout: 10 minutes (set in Makefile). diff --git a/tests/timepart_ins_perf.test/runit b/tests/timepart_ins_perf.test/runit new file mode 100755 index 0000000000..5377038cb0 --- /dev/null +++ b/tests/timepart_ins_perf.test/runit @@ -0,0 +1,381 @@ +#!/usr/bin/env bash +bash -n "$0" | exit 1 +source ${TESTSROOTDIR}/tools/runit_common.sh +. ${TESTSROOTDIR}/tools/hrtime.sh + +# Performance benchmark for TRUNCATE time partition unique index enforcement. +# +# Runs three workloads against an 8-shard partition, each comparing three +# configurations (off / on / on-nolocks): +# +# Phase 1 — INSERT: workers insert new rows into the partition. +# Cross-shard unique checks fire for every unique index. +# +# Phase 2 — UPDATE non-key column: workers update column c (not unique). +# Cross-shard checks do not fire; serves as a control. +# +# Phase 3 — DELETE: workers delete rows. +# Cross-shard checks do not fire; serves as a control. +# +# Each pass repopulates the partition to an identical initial state so +# all three configurations execute the same workload. +# +# The test only fails on operational errors. + +dbname=$1 +cmd="cdb2sql ${CDB2_OPTIONS} $dbname default" +cmdt="cdb2sql -tabs ${CDB2_OPTIONS} $dbname default" + +# Configurable parameters — override on the make command line: +# make timepart_ins_perf ROWS_PER_SHARD=1000 NRANGES=4 NWORKERS=4 ITERATIONS=3 +ROWS_PER_SHARD=${ROWS_PER_SHARD:-10000} +NRANGES=${NRANGES:-8} +ITERATIONS=${ITERATIONS:-3} +D_SIZE=${D_SIZE:-16} # bytes for randomblob(); d column = hex = 2*D_SIZE chars +TXN_SIZE=${TXN_SIZE:-10} # rows per transaction +DEBUG_TRACE=${DEBUG_TRACE:-1} # 1=enable partition_unique_debug and collect detailed stats + +RETENTION=8 +START='20260101T' + +# ============================================================ +# Setup +# ============================================================ + +run_or_die() +{ + local sql=$1 msg=$2 + $cmd "$sql" > /dev/null || failexit "$msg" +} + +D_COL_SIZE=$(( 2 * D_SIZE + 1 )) + +run_or_die \ + "create table t ( + a int, + b int, + c int, + d cstring(${D_COL_SIZE}), + unique (a), + unique (b) + ) partitioned by time period 'daily' retention ${RETENTION} start '${START}'" \ + "create partitioned table failed" + +nshards=$($cmdt "select count(*) from comdb2_timepartshards where name='t'") +if [[ "$nshards" != "$RETENTION" ]]; then + failexit "expected ${RETENTION} shards, got '${nshards}'" +fi + +echo "Partition: ${RETENTION} shards, ${ROWS_PER_SHARD} rows/shard" +echo "Row size: d = hex(randomblob(${D_SIZE})) = ${D_COL_SIZE} chars" +echo "" + +# Collect shard names (sorted = oldest first, current = last) +mapfile -t shard_names < <($cmdt "select shardname from comdb2_timepartshards where name='t' order by shardname") + +# ============================================================ +# Seed all shards with background data +# ============================================================ +# Each shard s gets a/b in [s*ROWS_PER_SHARD+1 .. (s+1)*ROWS_PER_SHARD]. +# These rows stay throughout the benchmark so cross-shard BTree lookups +# traverse real pages. + +SEED_TOTAL=$(( RETENTION * ROWS_PER_SHARD )) + +seed_shards() { + local msg=${1:-"Seeding shards..."} + echo "$msg" + local t0=$(timems) + for (( s = 0; s < ${#shard_names[@]}; s++ )); do + local shard=${shard_names[$s]} + local base=$(( s * ROWS_PER_SHARD )) + $cmd "insert into \"${shard}\" + select value+${base}, value+${base}, value, hex(randomblob(${D_SIZE})) + from generate_series(1, ${ROWS_PER_SHARD})" > /dev/null \ + || failexit "seed shard '${shard}' failed" + done + echo "All shards seeded in $(( $(timems) - t0 )) ms" +} + +seed_shards "Seeding shards..." + +total=$($cmdt "select count(*) from t") +[[ "$total" != "$SEED_TOTAL" ]] && failexit "expected ${SEED_TOTAL} seed rows, got '${total}'" +echo "Seed rows verified: ${total}" +echo "" + +# ============================================================ +# Benchmark rows: inserted above the seed range +# ============================================================ +# Workers r=0..NRANGES-1 each own a non-overlapping range of RANGE_SIZE rows +# with a/b values starting just above the seeded range. +# These rows are inserted/updated/deleted during the benchmark passes. + +NWORKERS=${NWORKERS:-$NRANGES} +RANGE_SIZE=$(( ROWS_PER_SHARD / NRANGES )) +BENCH_BASE=$SEED_TOTAL # benchmark rows: a = BENCH_BASE + r*RANGE_SIZE + 1..RANGE_SIZE +BENCH_ROWS=$(( NWORKERS * RANGE_SIZE )) + +echo "Benchmark: ${NWORKERS} parallel workers, ${ITERATIONS} iterations, ${RANGE_SIZE} rows/range, ${TXN_SIZE} rows/txn" +echo "(each iteration: order of off/on passes chosen randomly to equalise cache warming)" +echo "" + +# ============================================================ +# Pre-generate SQL files (outside timing) +# ============================================================ + +SQL_DIR=$(mktemp -d) || failexit "mktemp -d failed" +trap "rm -rf '${SQL_DIR}'" EXIT + +echo "Pre-generating SQL files (${NWORKERS} workers x 3 operations)..." + +for (( w = 0; w < NWORKERS; w++ )); do + wbase=$(( BENCH_BASE + w * RANGE_SIZE )) + + # INSERT + { + for (( k = 0; k < RANGE_SIZE; k++ )); do + val=$(( wbase + k + 1 )) + (( k % TXN_SIZE == 0 )) && echo "begin;" + echo "insert into t values (${val}, ${val}, 0, hex(randomblob(${D_SIZE})));" + (( (k+1) % TXN_SIZE == 0 || k+1 == RANGE_SIZE )) && echo "commit;" + done + } > "${SQL_DIR}/ins_w${w}.sql" + + # UPDATE non-key (c = c + 1) + { + for (( k = 0; k < RANGE_SIZE; k++ )); do + val=$(( wbase + k + 1 )) + (( k % TXN_SIZE == 0 )) && echo "begin;" + echo "update t set c = c + 1 where a = ${val};" + (( (k+1) % TXN_SIZE == 0 || k+1 == RANGE_SIZE )) && echo "commit;" + done + } > "${SQL_DIR}/upd_w${w}.sql" + + # DELETE + { + for (( k = 0; k < RANGE_SIZE; k++ )); do + val=$(( wbase + k + 1 )) + (( k % TXN_SIZE == 0 )) && echo "begin;" + echo "delete from t where a = ${val};" + (( (k+1) % TXN_SIZE == 0 || k+1 == RANGE_SIZE )) && echo "commit;" + done + } > "${SQL_DIR}/del_w${w}.sql" +done +echo "Done." +echo "" + +# ============================================================ +# Helpers: tunables, stats, repopulate +# ============================================================ + +set_one_tunable() { + local name=$1 val=$2 + if [[ -n "$CLUSTER" ]]; then + for node in $CLUSTER; do + cdb2sql ${CDB2_OPTIONS} --host $node $dbname default \ + "put tunable $name $val" > /dev/null \ + || failexit "put tunable $name $val on $node" + done + else + $cmd "put tunable $name $val" > /dev/null \ + || failexit "put tunable $name $val" + fi +} + +set_tunable() { + local val=$1 + set_one_tunable partition_unique $val + set_one_tunable partition_unique_debug $DEBUG_TRACE + set_one_tunable partition_unique_skip_locks 0 +} + +set_tunable_nolocks() { + set_one_tunable partition_unique 1 + set_one_tunable partition_unique_debug $DEBUG_TRACE + set_one_tunable partition_unique_skip_locks 1 +} + +get_stats() { + local total=0 + if [[ -n "$CLUSTER" ]]; then + for node in $CLUSTER; do + local v=$(cdb2sql -tabs ${CDB2_OPTIONS} --host $node $dbname default \ + "select cast(value as int) from comdb2_metrics where name='$1'") + total=$(( total + v )) + done + else + total=$($cmdt "select cast(value as int) from comdb2_metrics where name='$1'") + fi + echo $total +} + +clear_lock_stats() { + if [[ -n "$CLUSTER" ]]; then + for node in $CLUSTER; do + cdb2sql ${CDB2_OPTIONS} --host $node $dbname default \ + "exec procedure sys.cmd.send('clear_lock_stats')" > /dev/null + done + else + $cmd "exec procedure sys.cmd.send('clear_lock_stats')" > /dev/null + fi +} + +# Delete all benchmark rows then re-insert them (for UPDATE/DELETE passes) +repopulate_bench() { + $cmd "delete from t where a > ${BENCH_BASE}" > /dev/null \ + || failexit "repopulate_bench: delete failed" + for (( w = 0; w < NWORKERS; w++ )); do + local wbase=$(( BENCH_BASE + w * RANGE_SIZE )) + $cmd "insert into t + select value+${wbase}, value+${wbase}, 0, hex(randomblob(${D_SIZE})) + from generate_series(1, ${RANGE_SIZE})" > /dev/null \ + || failexit "repopulate_bench: insert worker ${w} failed" + done +} + +# Delete all benchmark rows (for INSERT passes — they are about to be inserted) +cleanup_bench() { + $cmd "delete from t where a > ${BENCH_BASE}" > /dev/null \ + || failexit "cleanup_bench: delete failed" +} + +# run_workers : run NWORKERS parallel workers, return pass_ms +run_workers() { + local prefix=$1 + local pids=() failed=0 + local t0=$(timems) + for (( w = 0; w < NWORKERS; w++ )); do + cdb2sql ${CDB2_OPTIONS} $dbname default \ + -f "${SQL_DIR}/${prefix}_w${w}.sql" > /dev/null & + pids+=($!) + done + for pid in "${pids[@]}"; do wait "$pid" || failed=1; done + pass_ms=$(( $(timems) - t0 )) + [[ $failed -ne 0 ]] && failexit "${prefix} workers failed" +} + +collect_stats() { + local pfx=$1 + eval "${pfx}_dl=$(get_stats deadlocks)" + eval "${pfx}_lw=$(get_stats lockwaits)" + eval "${pfx}_lr=$(get_stats lockrequests)" + if (( DEBUG_TRACE )); then + eval "${pfx}_lrd=$(get_stats lockreqs_deadlocked)" + eval "${pfx}_pdt=$(get_stats lockreqs_in_pdt)" + eval "${pfx}_dka=$(get_stats dka_calls)" + eval "${pfx}_csc=$(get_stats cross_shard_calls)" + else + eval "${pfx}_lrd=0; ${pfx}_pdt=0; ${pfx}_dka=0; ${pfx}_csc=0" + fi +} + +pct() { + local val=$1 base=$2 + if (( base == 0 )); then echo "n/a"; return; fi + local diff=$(( val - base )) p=$(( (val - base) * 100 / base )) + if (( diff >= 0 )); then printf "+%d%%" $p; else printf "%d%%" $p; fi +} + +# ============================================================ +# run_benchmark