Changeset: 2afe189425a1 for MonetDB URL: https://dev.monetdb.org/hg/MonetDB/rev/2afe189425a1 Added Files: sql/test/miscellaneous/Tests/transaction_isolation4.SQL.py Modified Files: sql/storage/bat/bat_storage.c sql/test/miscellaneous/Tests/All Branch: iso Log Message:
Another transaction isolation case to fix. For tables that don't support concurrent updates, hold the lock between checking segments and claiming them diffs (164 lines): diff --git a/sql/storage/bat/bat_storage.c b/sql/storage/bat/bat_storage.c --- a/sql/storage/bat/bat_storage.c +++ b/sql/storage/bat/bat_storage.c @@ -3476,7 +3476,7 @@ add_offsets(BAT *pos, BUN slot, size_t n } static BAT * -claim_segmentsV2(sql_trans *tr, sql_table *t, storage *s, size_t cnt) +claim_segmentsV2(sql_trans *tr, sql_table *t, storage *s, size_t cnt, bool locked) { int in_transaction = segments_in_transaction(tr, t), ok = LOG_OK; assert(s->segs); @@ -3485,7 +3485,8 @@ claim_segmentsV2(sql_trans *tr, sql_tabl BAT *pos = NULL; size_t total = cnt; - lock_table(tr->store, t->base.id); + if (!locked) + lock_table(tr->store, t->base.id); /* naive vacuum approach, iterator through segments, use deleted segments or create new segment at the end */ for (segment *seg = s->segs->h, *p = NULL; seg && cnt && ok == LOG_OK; p = seg, seg = seg->next) { if (seg->deleted && seg->ts < oldest && seg->end > seg->start) { /* re-use old deleted or rolledback append */ @@ -3537,7 +3538,8 @@ claim_segmentsV2(sql_trans *tr, sql_tabl if (!pos) ok = LOG_ERR; } - unlock_table(tr->store, t->base.id); + if (!locked) + unlock_table(tr->store, t->base.id); /* hard to only add this once per transaction (probably want to change to once per new segment) */ if ((!inTransaction(tr, t) && !in_transaction && isGlobal(t)) || (!isNew(t) && isLocalTemp(t))) { @@ -3558,17 +3560,18 @@ claim_segmentsV2(sql_trans *tr, sql_tabl } static BAT * -claim_segments(sql_trans *tr, sql_table *t, storage *s, size_t cnt) +claim_segments(sql_trans *tr, sql_table *t, storage *s, size_t cnt, bool locked) { if (cnt > 1) - return claim_segmentsV2(tr, t, s, cnt); + return claim_segmentsV2(tr, t, s, cnt, locked); int in_transaction = segments_in_transaction(tr, t), ok = LOG_OK; assert(s->segs); ulng oldest = store_oldest(tr->store); BUN slot = 0; int reused = 0; - lock_table(tr->store, t->base.id); + if (!locked) + lock_table(tr->store, t->base.id); /* naive vacuum approach, iterator through segments, check for large enough deleted segments * or create new segment at the end */ for (segment *seg = s->segs->h, *p = NULL; seg && ok == LOG_OK; p = seg, seg = seg->next) { @@ -3606,7 +3609,8 @@ claim_segments(sql_trans *tr, sql_table slot = s->segs->t->start; } } - unlock_table(tr->store, t->base.id); + if (!locked) + unlock_table(tr->store, t->base.id); /* hard to only add this once per transaction (probably want to change to once per new segment) */ if ((!inTransaction(tr, t) && !in_transaction && isGlobal(t)) || (!isNew(t) && isLocalTemp(t))) { @@ -3635,15 +3639,16 @@ claim_tab(sql_trans *tr, sql_table *t, s if ((s = bind_del_data(tr, t, NULL)) == NULL) return NULL; - return claim_segments(tr, t, s, cnt); /* find slot(s) */ + return claim_segments(tr, t, s, cnt, false); /* find slot(s) */ } -/* some tables cannot be updates at the concurrently (user/roles etc) */ +/* some tables cannot be updated concurrently (user/roles etc) */ static void * key_claim_tab(sql_trans *tr, sql_table *t, size_t cnt) { storage *s; - int ok = LOG_OK; + int res = 0; + BAT *b = NULL; /* we have a single segment structure for each persistent table * for temporary tables each has its own */ @@ -3652,27 +3657,28 @@ key_claim_tab(sql_trans *tr, sql_table * return NULL; lock_table(tr->store, t->base.id); - ok = segments_conflict(tr, s->segs, 1); + if ((res = segments_conflict(tr, s->segs, 1))) { + unlock_table(tr->store, t->base.id); + return NULL; + } + b = claim_segments(tr, t, s, cnt, true); /* find slot(s) */ unlock_table(tr->store, t->base.id); - - if (ok != LOG_OK) - return NULL; - return claim_segments(tr, t, s, cnt); /* find slot(s) */ + return b; } static int tab_validate(sql_trans *tr, sql_table *t, int uncommitted) { storage *s; - int ok = LOG_OK; + int res = 0; if ((s = bind_del_data(tr, t, NULL)) == NULL) return LOG_ERR; lock_table(tr->store, t->base.id); - ok = segments_conflict(tr, s->segs, uncommitted); + res = segments_conflict(tr, s->segs, uncommitted); unlock_table(tr->store, t->base.id); - return ok; + return res ? LOG_CONFLICT : LOG_OK; } static BAT * diff --git a/sql/test/miscellaneous/Tests/All b/sql/test/miscellaneous/Tests/All --- a/sql/test/miscellaneous/Tests/All +++ b/sql/test/miscellaneous/Tests/All @@ -22,3 +22,4 @@ prepare transaction_isolation transaction_isolation2 transaction_isolation3 +transaction_isolation4 diff --git a/sql/test/miscellaneous/Tests/transaction_isolation4.SQL.py b/sql/test/miscellaneous/Tests/transaction_isolation4.SQL.py new file mode 100644 --- /dev/null +++ b/sql/test/miscellaneous/Tests/transaction_isolation4.SQL.py @@ -0,0 +1,30 @@ +from MonetDBtesting.sqltest import SQLTestCase + +with SQLTestCase() as mdb1: + with SQLTestCase() as mdb2: + mdb1.connect(username="monetdb", password="monetdb") + mdb2.connect(username="monetdb", password="monetdb") + + mdb1.execute("create table myt (i int, j int);").assertSucceeded() + mdb1.execute('start transaction;').assertSucceeded() + mdb2.execute('start transaction;').assertSucceeded() + mdb1.execute("alter table myt add primary key (i);").assertSucceeded() + mdb2.execute("alter table myt add primary key (j);").assertFailed(err_code="42000", err_message="NOT NULL CONSTRAINT: transaction conflict detected") # only one pk per table + mdb1.execute('commit;').assertSucceeded() + mdb2.execute('rollback;').assertSucceeded() + + mdb1.execute('CREATE schema mys;').assertSucceeded() + mdb1.execute("CREATE USER duser WITH PASSWORD 'ups' NAME 'ups' SCHEMA mys;").assertSucceeded() + mdb1.execute("create table mys.myt2 (i int, j int);").assertSucceeded() + + mdb1.execute('start transaction;').assertSucceeded() + mdb2.execute('start transaction;').assertSucceeded() + mdb1.execute("GRANT SELECT on table mys.myt2 to duser;").assertSucceeded() + mdb2.execute('drop user duser;').assertSucceeded() + mdb1.execute('commit;').assertSucceeded() + mdb2.execute('commit;').assertFailed(err_code="40000", err_message="COMMIT: transaction is aborted because of concurrency conflicts, will ROLLBACK instead") + + mdb1.execute('start transaction;').assertSucceeded() + mdb1.execute('drop table myt;').assertSucceeded() + mdb1.execute('drop schema mys cascade;').assertSucceeded() + mdb1.execute('commit;').assertSucceeded() _______________________________________________ checkin-list mailing list checkin-list@monetdb.org https://www.monetdb.org/mailman/listinfo/checkin-list