mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-19 09:40:12 +00:00
feat(agentcompat): add SQLite hold scoped attribution
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
//go:build agentcompat && linux
|
||||
|
||||
package singleton
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type sqliteAttributionBlockingTx struct {
|
||||
commitStarted chan struct{}
|
||||
allowCommit chan struct{}
|
||||
commitCalls atomic.Int32
|
||||
rollbackCalls atomic.Int32
|
||||
lifecycleMu *sync.Mutex
|
||||
lockFree atomic.Bool
|
||||
}
|
||||
|
||||
func (transaction *sqliteAttributionBlockingTx) Commit() error {
|
||||
transaction.commitCalls.Add(1)
|
||||
// Probe before publishing entry so losing terminal calls cannot contend with this boundary check.
|
||||
if transaction.lifecycleMu.TryLock() {
|
||||
transaction.lockFree.Store(true)
|
||||
transaction.lifecycleMu.Unlock()
|
||||
}
|
||||
close(transaction.commitStarted)
|
||||
<-transaction.allowCommit
|
||||
return nil
|
||||
}
|
||||
|
||||
func (transaction *sqliteAttributionBlockingTx) Rollback() error {
|
||||
transaction.rollbackCalls.Add(1)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestSQLiteAttributionTransactionCompletionRunsRawCommitExactlyOnce(t *testing.T) {
|
||||
// Given
|
||||
tracker := NewSQLiteHoldTracker()
|
||||
identity := sqliteHoldTestTransaction(201)
|
||||
if err := tracker.BeginSQLiteTransaction(identity); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw := &sqliteAttributionBlockingTx{commitStarted: make(chan struct{}), allowCommit: make(chan struct{})}
|
||||
state := &sqliteAttributionTransaction{
|
||||
transaction: identity,
|
||||
raw: raw,
|
||||
tracker: tracker,
|
||||
journalFD: -1,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
var rawCloseCalls atomic.Int32
|
||||
connection := &sqliteAttributionConnection{transaction: state}
|
||||
connection.closeRawConnection = func() error {
|
||||
if !connection.lifecycleMu.TryLock() {
|
||||
return errors.New("raw connection Close ran while lifecycle lock was held")
|
||||
}
|
||||
connection.lifecycleMu.Unlock()
|
||||
rawCloseCalls.Add(1)
|
||||
return nil
|
||||
}
|
||||
raw.lifecycleMu = &connection.lifecycleMu
|
||||
owner := &sqliteAttributionTx{connection: connection, state: state}
|
||||
secondCommit := &sqliteAttributionTx{connection: connection, state: state}
|
||||
commitResult := make(chan error, 1)
|
||||
secondCommitResult := make(chan error, 1)
|
||||
rollbackResult := make(chan error, 1)
|
||||
closeResult := make(chan error, 1)
|
||||
secondCommitStarted := make(chan struct{})
|
||||
rollbackStarted := make(chan struct{})
|
||||
closeStarted := make(chan struct{})
|
||||
|
||||
// When
|
||||
go func() { commitResult <- owner.Commit() }()
|
||||
<-raw.commitStarted
|
||||
go func() {
|
||||
close(secondCommitStarted)
|
||||
secondCommitResult <- secondCommit.Commit()
|
||||
}()
|
||||
go func() {
|
||||
close(rollbackStarted)
|
||||
rollbackResult <- owner.Rollback()
|
||||
}()
|
||||
go func() {
|
||||
close(closeStarted)
|
||||
closeResult <- connection.Close()
|
||||
}()
|
||||
<-secondCommitStarted
|
||||
<-rollbackStarted
|
||||
<-closeStarted
|
||||
for _, result := range []<-chan error{secondCommitResult, rollbackResult} {
|
||||
select {
|
||||
case err := <-result:
|
||||
t.Fatalf("completion loser returned before raw Commit completion: %v", err)
|
||||
default:
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-state.done:
|
||||
t.Fatal("completion published before raw Commit was allowed to finish")
|
||||
default:
|
||||
}
|
||||
if calls := rawCloseCalls.Load(); calls != 0 {
|
||||
t.Fatalf("raw connection Close calls while raw Commit was in flight = %d, want 0", calls)
|
||||
}
|
||||
if !raw.lockFree.Load() {
|
||||
t.Fatal("raw Commit ran while lifecycle lock was held")
|
||||
}
|
||||
if !sqliteAttributionTrackerTransactionActive(tracker, identity) {
|
||||
t.Fatal("tracker transaction became inactive while raw Commit was blocked")
|
||||
}
|
||||
close(raw.allowCommit)
|
||||
commitErr := <-commitResult
|
||||
secondCommitErr := <-secondCommitResult
|
||||
rollbackErr := <-rollbackResult
|
||||
closeErr := <-closeResult
|
||||
|
||||
// Then
|
||||
if commitErr != nil {
|
||||
t.Fatalf("raw Commit error = %v", commitErr)
|
||||
}
|
||||
for _, loserErr := range []error{secondCommitErr, rollbackErr} {
|
||||
if !errors.Is(loserErr, driver.ErrBadConn) {
|
||||
t.Fatalf("completion loser error = %v, want driver.ErrBadConn", loserErr)
|
||||
}
|
||||
}
|
||||
if closeErr != nil && !errors.Is(closeErr, driver.ErrBadConn) {
|
||||
t.Fatalf("connection Close error = %v, want nil or driver.ErrBadConn", closeErr)
|
||||
}
|
||||
select {
|
||||
case <-state.done:
|
||||
default:
|
||||
t.Fatal("completion signal did not close after raw Commit finished")
|
||||
}
|
||||
if calls := raw.commitCalls.Load(); calls != 1 {
|
||||
t.Fatalf("raw Commit calls = %d, want 1", calls)
|
||||
}
|
||||
if calls := rawCloseCalls.Load(); calls != 1 {
|
||||
t.Fatalf("raw connection Close calls after raw Commit completed = %d, want 1", calls)
|
||||
}
|
||||
if calls := raw.rollbackCalls.Load(); calls != 0 {
|
||||
t.Fatalf("raw Rollback calls while raw Commit owned completion = %d, want 0", calls)
|
||||
}
|
||||
if _, _, active := sqliteAttributionTransactionState(t, connection); active {
|
||||
t.Fatal("completed transaction remained attached to the connection")
|
||||
}
|
||||
if sqliteAttributionTrackerTransactionActive(tracker, identity) {
|
||||
t.Fatal("completed transaction remained active in the tracker")
|
||||
}
|
||||
}
|
||||
|
||||
var _ driver.Tx = (*sqliteAttributionBlockingTx)(nil)
|
||||
Reference in New Issue
Block a user