Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
74c5b70
transaction: Support file based transaction
pingyu Jun 9, 2026
5560ce5
fix build error
pingyu Jun 9, 2026
8db1362
fix CI errors
pingyu Jun 10, 2026
6c02671
Merge branch 'master' into txn-file-cse
pingyu Jul 6, 2026
f558577
address comments
pingyu Jul 6, 2026
44fa212
prepareTxnFileCommitTS
pingyu Jul 13, 2026
a209cbe
bo
pingyu Jul 14, 2026
d50071d
handle binlog
pingyu Jul 14, 2026
26bd55b
undetermined
pingyu Jul 14, 2026
70cee6e
resource group tag
pingyu Jul 15, 2026
b15c470
note for skip case
pingyu Jul 15, 2026
dd46c19
register metrics
pingyu Jul 15, 2026
06541e7
validate config
pingyu Jul 15, 2026
ace38d1
comment for GetMaxStartKey/GetMinEndKey
pingyu Jul 15, 2026
4f653f2
require -> assert
pingyu Jul 15, 2026
ad49ae2
Merge branch 'master' into txn-file-cse
pingyu Jul 15, 2026
c522a3e
Merge branch 'master' into txn-file-cse
pingyu Jul 28, 2026
707c2cf
fix CI
pingyu Jul 28, 2026
8b878c4
always get resource group tag
pingyu Aug 6, 2026
b30dcc8
handle primary not first
pingyu Aug 6, 2026
83ca973
no pipeline txn
pingyu Aug 6, 2026
88763ff
handle shared lock
pingyu Aug 6, 2026
46861ab
add lock_test
pingyu Aug 6, 2026
d82de8f
cleanup ctx
pingyu Aug 6, 2026
ebab9a9
MaxTxnChunkSizeInParallel
pingyu Aug 6, 2026
fdca77b
resource control
pingyu Aug 6, 2026
b8b8131
skip valid config
pingyu Aug 6, 2026
0d12389
txn file split region
pingyu Aug 6, 2026
9b6b05c
no txn file for shared lock
pingyu Aug 7, 2026
f0a60e9
handle assertion level
pingyu Aug 7, 2026
ae6b452
Merge remote-tracking branch 'upstream/master' into txn-file-cse
pingyu Aug 7, 2026
fd46635
txn file assertion
pingyu Aug 7, 2026
0bb1ee0
accouting error
pingyu Aug 7, 2026
8cf5a02
rollback key error
pingyu Aug 7, 2026
98432af
http close
pingyu Aug 7, 2026
1825891
discard value
pingyu Aug 7, 2026
77facf4
fix ci
pingyu Aug 8, 2026
3c5576b
Merge remote-tracking branch 'upstream/master' into txn-file-cse
pingyu Aug 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions config/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ const (
DefGrpcInitialConnWindowSize = 1 << 27 // 128MiB
DefMaxConcurrencyRequestLimit = math.MaxInt64
DefBatchPolicy = BatchPolicyStandard
// MaxTxnChunkSizeInParallel is the maximum total size of transaction chunks processed in parallel.
MaxTxnChunkSizeInParallel uint64 = 4 << 30 // 4GB
)

const (
Expand Down Expand Up @@ -121,6 +123,21 @@ type TiKVClient struct {

// RUV2 is the RU v2 TiKV-side weights used to calculate TiKV RU values from ExecDetailsV2.RuV2.
RUV2 RUV2TiKVConfig `toml:"ru-v2" json:"ru-v2"`

// TxnChunkWriterAddr is the address of the txn chunk writer for file-based txn.
TxnChunkWriterAddr string `toml:"txn-chunk-writer-addr" json:"txn-chunk-writer-addr"`
// TxnChunkWriterConcurrency is the concurrency to request the txn chunk writer for file-based txn.
TxnChunkWriterConcurrency uint `toml:"txn-chunk-writer-concurrency" json:"txn-chunk-writer-concurrency"`
// TxnChunkMaxSize is the maximum size of a txn chunk of file-based txn.
TxnChunkMaxSize uint64 `toml:"txn-chunk-max-size" json:"txn-chunk-max-size"`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe we should validate the new txn-file config values before accepting them, especially TxnChunkMaxSize > 0? If this is set to 0, the txn-file path can divide by zero when calculating chunk counts or parallelism, so rejecting or normalizing it in Valid() would make the failure mode clearer.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 06541e7.

// TxnFileMinMutationSize is the minimum size of mutations to use file-based txn.
TxnFileMinMutationSize uint64 `toml:"txn-file-min-mutation-size" json:"txn-file-min-mutation-size"`
// TxnFileRUDiscountRatio is the discount ratio of resource unit for file-based txn.
// Will be ignored if it's <= 0 or >= 1.
TxnFileRUDiscountRatio float64 `toml:"txn-file-ru-discount-ratio" json:"txn-file-ru-discount-ratio"`
// TxnFileRequestSourceWhitelist is the whitelist of request source types (RequestSource.RequestSourceType) that can use file-based txn.
// For internal requests only. External requests can always use file-based txn.
TxnFileRequestSourceWhitelist []string `toml:"txn-file-request-source-whitelist" json:"txn-file-request-source-whitelist"`
}

// RUV2TiKVConfig is the configuration for RU v2 TiKV-side weight calculation.
Expand Down Expand Up @@ -232,6 +249,12 @@ func DefaultTiKVClient() TiKVClient {
MaxConcurrencyRequestLimit: DefMaxConcurrencyRequestLimit,
EnableReplicaSelectorV2: true,
RUV2: DefaultRUV2TiKVConfig(),

TxnChunkWriterConcurrency: 4,
TxnChunkMaxSize: 128 * 1024 * 1024,
TxnFileMinMutationSize: 16 * 1024 * 1024,
TxnFileRUDiscountRatio: 0.125, // filed-based txn costs 1/8 RU of normal txn.
TxnFileRequestSourceWhitelist: []string{},
}
}

Expand All @@ -246,6 +269,29 @@ func (config *TiKVClient) Valid() error {
if config.GetGrpcKeepAliveTimeout() < time.Millisecond*50 {
return fmt.Errorf("grpc-keepalive-timeout should be at least 0.05, but got %f", config.GrpcKeepAliveTimeout)
}
return validateTxnFileConfig(config)

@wfxr wfxr Aug 3, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

P2: Could txn-file validation be skipped while the feature is disabled?

TiKVClient.Valid now validates TxnChunkMaxSize and TxnChunkWriterConcurrency even when TxnChunkWriterAddr is empty, which currently means txn-file is disabled. Is this intended?

An existing caller that constructs TiKVClient directly, or decodes an old configuration into a zero-valued struct, can now fail validation without using txn-file at all. Callers starting from DefaultTiKVClient are unaffected because these fields already have defaults, but client-go consumers are not necessarily required to do that.

Would it make sense to validate these txn-file-only fields only when TxnChunkWriterAddr is configured, or normalize the zero values before validation? A feature-off compatibility test using an old-style configuration would also help preserve this behavior.

@pingyu pingyu Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed by b8b8131

}

func validateTxnFileConfig(config *TiKVClient) error {
if config.TxnChunkWriterAddr == "" {
// Skip validation when txn file is not enabled.
return nil
}
if config.TxnChunkMaxSize == 0 {
return fmt.Errorf("txn-chunk-max-size should be greater than 0")
}
if config.TxnChunkMaxSize > math.MaxInt {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

P2: Should reject chunk sizes that make the parallelism limit zero

This validation still accepts TxnChunkMaxSize values greater than MaxTxnChunkSizeInParallel (4 GiB). For such a valid value, 4 GiB / TxnChunkMaxSize becomes 0 and NewRateLimit(0) deadlocks the multi-secondary path. Please reject values above the parallel budget and defensively clamp the calculated rate limit to at least 1.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed by ebab9a9

return fmt.Errorf("txn-chunk-max-size should not exceed %d, but got %d", math.MaxInt, config.TxnChunkMaxSize)
}
if config.TxnChunkMaxSize > MaxTxnChunkSizeInParallel {
return fmt.Errorf("txn-chunk-max-size should not exceed %d, but got %d", MaxTxnChunkSizeInParallel, config.TxnChunkMaxSize)
}
if config.TxnChunkWriterConcurrency == 0 {
return fmt.Errorf("txn-chunk-writer-concurrency should be greater than 0")
}
if config.TxnChunkWriterConcurrency > math.MaxInt {
return fmt.Errorf("txn-chunk-writer-concurrency should not exceed %d, but got %d", math.MaxInt, config.TxnChunkWriterConcurrency)
}
return nil
}

Expand Down
81 changes: 81 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
package config

import (
"fmt"
"math"
"testing"
"time"

Expand Down Expand Up @@ -89,3 +91,82 @@ func TestValidateGRPCKeepAliveTimeout(t *testing.T) {
assert.NotNil(t, cfg.Valid())
assert.Equal(t, "grpc-keepalive-timeout should be at least 0.05, but got 0.040000", cfg.Valid().Error())
}

func TestValidateTxnFileConfig(t *testing.T) {
configWithoutTxnFile := DefaultTiKVClient()
configWithoutTxnFile.TxnChunkMaxSize = 0
assert.NoError(t, configWithoutTxnFile.Valid())

maxInt := uint64(math.MaxInt)
tests := []struct {
name string
configure func(*TiKVClient)
err string
}{
{
name: "default",
},
{
name: "zero chunk size",
configure: func(cfg *TiKVClient) {
cfg.TxnChunkMaxSize = 0
},
err: "txn-chunk-max-size should be greater than 0",
},
{
name: "maximum chunk size",
configure: func(cfg *TiKVClient) {
cfg.TxnChunkMaxSize = MaxTxnChunkSizeInParallel
},
},
{
name: "chunk size exceeds parallel budget",
configure: func(cfg *TiKVClient) {
cfg.TxnChunkMaxSize = 4<<30 + 1
},
err: fmt.Sprintf("txn-chunk-max-size should not exceed %d, but got %d", uint64(4<<30), uint64(4<<30)+1),
},
{
name: "chunk size exceeds int",
configure: func(cfg *TiKVClient) {
cfg.TxnChunkMaxSize = maxInt + 1
},
err: fmt.Sprintf("txn-chunk-max-size should not exceed %d, but got %d", maxInt, maxInt+1),
},
{
name: "zero writer concurrency",
configure: func(cfg *TiKVClient) {
cfg.TxnChunkWriterConcurrency = 0
},
err: "txn-chunk-writer-concurrency should be greater than 0",
},
{
name: "maximum writer concurrency",
configure: func(cfg *TiKVClient) {
cfg.TxnChunkWriterConcurrency = uint(maxInt)
},
},
{
name: "writer concurrency exceeds int",
configure: func(cfg *TiKVClient) {
cfg.TxnChunkWriterConcurrency = uint(maxInt) + 1
},
err: fmt.Sprintf("txn-chunk-writer-concurrency should not exceed %d, but got %d", maxInt, uint(maxInt)+1),
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cfg := DefaultTiKVClient()
if test.configure != nil {
test.configure(&cfg)
}
if test.err == "" {
assert.NoError(t, cfg.Valid())
return
}
cfg.TxnChunkWriterAddr = "127.0.0.1"
assert.EqualError(t, cfg.Valid(), test.err)
})
}
}
29 changes: 29 additions & 0 deletions integration_tests/assertion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/pingcap/failpoint"
"github.com/pingcap/kvproto/pkg/kvrpcpb"
"github.com/stretchr/testify/suite"
"github.com/tikv/client-go/v2/config"
tikverr "github.com/tikv/client-go/v2/error"
"github.com/tikv/client-go/v2/kv"
"github.com/tikv/client-go/v2/oracle"
Expand Down Expand Up @@ -195,6 +196,34 @@ func (s *testAssertionSuite) TestPrewriteAssertion() {
s.testAssertionImpl(prefix+"c", true, true, kvrpcpb.AssertionLevel_Strict)
}

func (s *testAssertionSuite) TestPrewriteAssertionWithTxnFileEnabled() {
ts, err := s.store.CurrentTimestamp(oracle.GlobalTxnScope)
s.Require().NoError(err)
key := encodeKey("~assertion", fmt.Sprintf("test-txn-file-assertion-%d", ts))

prepareTxn, err := s.store.Begin()
s.Require().NoError(err)
s.Require().NoError(prepareTxn.Set(key, []byte("existing")))
s.Require().NoError(prepareTxn.Commit(context.Background()))

restore := config.UpdateGlobal(func(conf *config.Config) {
conf.TiKVClient.TxnChunkWriterAddr = "127.0.0.1"
conf.TiKVClient.TxnFileMinMutationSize = 1
})
s.T().Cleanup(restore)

txn, err := s.store.Begin()
s.Require().NoError(err)
txn.SetAssertionLevel(kvrpcpb.AssertionLevel_Strict)
s.Require().NoError(txn.GetMemBuffer().SetWithFlags(key, []byte("updated"), kv.SetAssertNotExist))

err = txn.Commit(context.Background())
assertionFailed, ok := errors.Cause(err).(*tikverr.ErrAssertionFailed)
s.Require().True(ok, "expected ErrAssertionFailed, got %v", err)
s.Equal(key, assertionFailed.Key)
s.Equal(kvrpcpb.Assertion_NotExist, assertionFailed.Assertion)
}

func (s *testAssertionSuite) TestFastAssertion() {
// When the test cases runs with TiKV, the TiKV cluster can be reused, thus there may be deleted versions caused by
// previous tests. This test case may meet different behavior if there are deleted versions. To avoid it, compose a
Expand Down
55 changes: 55 additions & 0 deletions integration_tests/shared_lock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,61 @@ func (s *testSharedLockSuite) TestPrewriteResolveExpiredSharedLock() {
s.Nil(txn1.Rollback())
}

func (s *testSharedLockSuite) TestPrewriteResolveExpiredSharedLockWithActiveHolder() {
originManagedLockTTL := atomic.LoadUint64(&transaction.ManagedLockTTL)
atomic.StoreUint64(&transaction.ManagedLockTTL, 500)
defer atomic.StoreUint64(&transaction.ManagedLockTTL, originManagedLockTTL)

expiredTxn := s.begin()
activeTxn := s.begin()
sharedKey := s.key("TestPrewriteResolveExpiredSharedLockWithActiveHolder_key")
primaryKeys := [][]byte{
s.key("TestPrewriteResolveExpiredSharedLockWithActiveHolder_expired_pk"),
s.key("TestPrewriteResolveExpiredSharedLockWithActiveHolder_active_pk"),
}
for i, txn := range []transaction.TxnProbe{expiredTxn, activeTxn} {
s.Nil(txn.LockKeys(context.Background(), kv.NewLockCtx(s.getTS(), 1000, time.Now()), primaryKeys[i]))
lockCtx := kv.NewLockCtx(s.getTS(), 1000, time.Now())
lockCtx.InShareMode = true
s.Nil(txn.LockKeys(context.Background(), lockCtx, sharedKey))
}
s.waitLocks(sharedKey, s.getTS(), 2, "expect two shared lock holders")
expiredTxn.GetCommitter().CloseTTLManager()
time.Sleep(time.Duration(atomic.LoadUint64(&transaction.ManagedLockTTL))*time.Millisecond + 200*time.Millisecond)
s.True(activeTxn.GetCommitter().IsTTLRunning())

contender, err := s.store.Begin()
s.Nil(err)
value := []byte("contender-value")
s.Nil(contender.Set(sharedKey, value))
commitDone := make(chan error, 1)
go func() {
commitDone <- contender.Commit(context.Background())
}()

locks := s.waitLocks(sharedKey, s.getTS(), 1, "expired holder should be removed while active holder remains")
s.Equal(activeTxn.StartTS(), locks[0].TxnID)
select {
case err := <-commitDone:
s.FailNow("prewrite returned while the active shared lock remained", err)
case <-time.After(200 * time.Millisecond):
}

s.Nil(activeTxn.Rollback())
select {
case err := <-commitDone:
s.Nil(err)
case <-time.After(5 * time.Second):
s.FailNow("prewrite did not finish after the active holder released")
}

snapshot := s.store.GetSnapshot(contender.CommitTS())
got, err := snapshot.Get(context.Background(), sharedKey)
s.Nil(err)
s.Equal(value, got.Value)
s.Nil(expiredTxn.Rollback())
}

func (s *testSharedLockSuite) TestForceLockRetryOnSharedLock() {
if config.NextGen {
s.T().Skip("NextGen does not support allow_lock_with_conflict / ForceLock yet")
Expand Down
Loading
Loading