Skip to content

logical: add the txnscheduler package#164544

Merged
trunk-io[bot] merged 1 commit intocockroachdb:masterfrom
jeffswenson:jeffswenson-txn-scheduler
Mar 5, 2026
Merged

logical: add the txnscheduler package#164544
trunk-io[bot] merged 1 commit intocockroachdb:masterfrom
jeffswenson:jeffswenson-txn-scheduler

Conversation

@jeffswenson
Copy link
Collaborator

The txnscheduler takes transactions with replication locks and transforms them into a sequence of transactions with explicit cross-transaction dependency information.

Release note: none
Epic: CRDB-57649

@jeffswenson jeffswenson requested a review from msbutler February 27, 2026 20:26
@jeffswenson jeffswenson requested a review from a team as a code owner February 27, 2026 20:26
@jeffswenson jeffswenson requested review from andyyang890 and removed request for a team February 27, 2026 20:26
@trunk-io
Copy link
Contributor

trunk-io bot commented Feb 27, 2026

😎 Merged successfully - details.

@cockroach-teamcity
Copy link
Member

This change is Reviewable

@jeffswenson jeffswenson force-pushed the jeffswenson-txn-scheduler branch 2 times, most recently from 629217d to a5c6a74 Compare March 1, 2026 13:34
Copy link
Collaborator

@msbutler msbutler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! left mostly clarifying questions

// Schedule requires the lock hashes within each transaction to be unique.
// Duplicates must be filtered out at the lock synthesis phase.
func (s *Scheduler) Schedule(
transaction Transaction, scratch []hlc.Timestamp,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: rename scratch to dependenciesBuffer so the caller understands its purpose?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

for _, lock := range txn.Locks {
locks, exists := s.lockMap[lock.Hash]
if !exists {
continue
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could add a little docstring here to explain why this is ok, or you could leave it as an exercise for the reader if you want.

This can happen if a subsequent write overwrote the lock table entry and committed before the earlier txn. For example:

  • t3 depends on t1 for key A
  • t2 depends on t1 for Key A

and t3 applies before t2, which will clean up the lock table for Key A before t2 applies.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a test only assertion here. This branch should never be taken because we don't remove applied transactions from the scheduler, we only ever advance the whole frontier as we need to reclaim memory.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, right. got my wires crossed here.

}
if head.readLocks == 0 {
nextHead := head.next
lt.locks[nextHead].writeLock = head.writeLock
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i may be misreading things, but i don't understand how head could have a writeLock. Suppose we're removing read locks for txn1, we know:

  • txn1 is the oldest timestamp in the lock table
  • we know that newer write locks clear out older read locks for a given key when recorded

Therefore, when removing old read locks on a given key, we should not encounter any write locks, right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this and added a note.

func BenchmarkScheduler_Schedule(b *testing.B) {
for _, txnSize := range []int{1, 10, 50, 1000} {
b.Run(fmt.Sprintf("size=%d", txnSize), func(b *testing.B) {
transactions := createRandomTransactions(b.N/txnSize, txnSize, 10000, 0.8)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what have you been setting N to in your benchmarks? I hope is much larger than 10,000 the size of the keyspace, to ensure the benchmark exercises key collisions/long read lock chains.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I let go pick this, but it usually ends up being several million.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It ends up being several million.

time int
readLocks []int
writeLocks []int
dependsOn []int
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: rename to expectedDependencies and expectedHorizon to make it clearer these validation fields.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

"github.com/stretchr/testify/require"
)

func TestScheduler(t *testing.T) {
Copy link
Collaborator

@msbutler msbutler Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be nice to build some sort of randomized scheduler test based on some DAG with read/write dependencies. This would ensure we have good coverage over lock_list.go. Not necessary for this PR.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, not a DAG: you could construct a 2D grid of discrete times and keys. Each entry in the grid is read lock, write lock or empty. You Could easily validate the schedule output with this I think.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a random test that uses a 2d oracle.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test looks great! If you haven't already done so, it could be worth sanity checking that the oracle deps look reasonable when table size is much lower.

for range rng.Intn(10) + 1 {
txn := makeTxn(false)

locks, horizon := scheduler.Schedule(txn, nil)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: rename locks, dependencies

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@jeffswenson jeffswenson force-pushed the jeffswenson-txn-scheduler branch 3 times, most recently from d217687 to be7dd80 Compare March 3, 2026 21:45
@jeffswenson jeffswenson requested a review from msbutler March 4, 2026 12:18
Copy link
Collaborator

@msbutler msbutler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! just a few tiny nits

"github.com/cockroachdb/cockroach/pkg/util/ring"
)

// NewScheduler constructs a scheduler that can track at least `size` locks.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

at most 'size' locks?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated this comment:

-// NewScheduler constructs a scheduler that can track at least `size` locks.
+// NewScheduler constructs a scheduler that can track at most `size` lock table
+// entries. Each lock table entry can track one write lock and up to 8 read
+// locks.

//
// | | Read Lock | Write Lock |
// |------------|-------------|------------|
// | Read Lock | Dependency | Dependency |
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no dependency with read x read

for _, lock := range txn.Locks {
locks, exists := s.lockMap[lock.Hash]
if !exists {
continue
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, right. got my wires crossed here.

"github.com/stretchr/testify/require"
)

func TestScheduler(t *testing.T) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test looks great! If you haven't already done so, it could be worth sanity checking that the oracle deps look reasonable when table size is much lower.

@jeffswenson jeffswenson force-pushed the jeffswenson-txn-scheduler branch from be7dd80 to ff7b772 Compare March 4, 2026 16:26
The txnscheduler takes transactions with replication locks and
transforms them into a sequence of transactions with explicit
cross-transaction dependency information.

Release note: none
Epic: CRDB-57649
@jeffswenson
Copy link
Collaborator Author

Here's an example of what a small oracle table looks like:

    scheduler_test.go:234: numTransactions=4 numLocks=4
    scheduler_test.go:256:         txn0    txn1    txn2    txn3
    scheduler_test.go:262: lock0     -       R       -       R
    scheduler_test.go:262: lock1     R       R       -       -
    scheduler_test.go:262: lock2     R       -       W       R
    scheduler_test.go:262: lock3     -       R       -       -
    scheduler_test.go:267: txn0 deps: []
    scheduler_test.go:267: txn1 deps: []
    scheduler_test.go:267: txn2 deps: [0.000000001,0]
    scheduler_test.go:267: txn3 deps: [0.000000003,0]

I spot checked a few of these and they look correct.

@jeffswenson jeffswenson force-pushed the jeffswenson-txn-scheduler branch from ff7b772 to 4bd3f09 Compare March 4, 2026 16:44
@jeffswenson
Copy link
Collaborator Author

Thanks for the review!

/trunk merge

@trunk-io trunk-io bot merged commit 2c65768 into cockroachdb:master Mar 5, 2026
37 of 38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants