Commit c38a81a4 authored by Michael de Hoog's avatar Michael de Hoog

Better receipt and send separation (fix potential deadlock)

parent bb2ccfab
...@@ -295,33 +295,44 @@ func (l *BatchSubmitter) loop() { ...@@ -295,33 +295,44 @@ func (l *BatchSubmitter) loop() {
select { select {
case <-ticker.C: case <-ticker.C:
l.loadBlocksIntoState(l.shutdownCtx) l.loadBlocksIntoState(l.shutdownCtx)
l.publishStateToL1(l.killCtx, queue, receiptsCh) l.publishStateToL1(queue, receiptsCh, false)
case r := <-receiptsCh: case r := <-receiptsCh:
l.handleReceipt(r) l.handleReceipt(r)
case <-l.shutdownCtx.Done(): case <-l.shutdownCtx.Done():
l.drainState(receiptsCh, queue) err := l.state.Close()
if err != nil {
l.log.Error("error closing the channel manager", "err", err)
}
l.publishStateToL1(queue, receiptsCh, true)
return return
} }
} }
} }
func (l *BatchSubmitter) drainState(receiptsCh chan txmgr.TxReceipt[txData], queue *txmgr.Queue[txData]) { // publishStateToL1 loops through the block data loaded into `state` and
err := l.state.Close() // submits the associated data to the L1 in the form of channel frames.
if err != nil { func (l *BatchSubmitter) publishStateToL1(queue *txmgr.Queue[txData], receiptsCh chan txmgr.TxReceipt[txData], drain bool) {
l.log.Error("error closing the channel manager", "err", err)
}
txDone := make(chan struct{}) txDone := make(chan struct{})
// send/wait and receipt reading must be on a separate goroutines to avoid deadlocks
go func() { go func() {
// publish remaining state defer func() {
l.publishStateToL1(l.killCtx, queue, receiptsCh) if drain {
// wait for all transactions to complete // if draining, we wait for all transactions to complete
queue.Wait() queue.Wait()
// notify that we're done }
close(txDone) close(txDone)
}()
for {
err := l.publishTxToL1(l.killCtx, queue, receiptsCh)
if err != nil {
if drain && err != io.EOF {
l.log.Error("error sending tx while draining state", "err", err)
}
return
}
}
}() }()
// drain and handle the remaining receipts
for { for {
select { select {
case r := <-receiptsCh: case r := <-receiptsCh:
...@@ -332,30 +343,28 @@ func (l *BatchSubmitter) drainState(receiptsCh chan txmgr.TxReceipt[txData], que ...@@ -332,30 +343,28 @@ func (l *BatchSubmitter) drainState(receiptsCh chan txmgr.TxReceipt[txData], que
} }
} }
// publishStateToL1 loops through the block data loaded into `state` and // publishTxToL1 submits a single state tx to the L1
// submits the associated data to the L1 in the form of channel frames. func (l *BatchSubmitter) publishTxToL1(ctx context.Context, queue *txmgr.Queue[txData], receiptsCh chan txmgr.TxReceipt[txData]) error {
func (l *BatchSubmitter) publishStateToL1(ctx context.Context, queue *txmgr.Queue[txData], receiptsCh chan txmgr.TxReceipt[txData]) {
// send all available transactions // send all available transactions
for { l1tip, err := l.l1Tip(ctx)
l1tip, err := l.l1Tip(ctx) if err != nil {
if err != nil { l.log.Error("Failed to query L1 tip", "error", err)
l.log.Error("Failed to query L1 tip", "error", err) return err
return }
} l.recordL1Tip(l1tip)
l.recordL1Tip(l1tip)
// Collect next transaction data
// Collect next transaction data txdata, err := l.state.TxData(l1tip.ID())
txdata, err := l.state.TxData(l1tip.ID()) if err == io.EOF {
if err == io.EOF { l.log.Trace("no transaction data available")
l.log.Trace("no transaction data available") return err
return } else if err != nil {
} else if err != nil { l.log.Error("unable to get tx data", "err", err)
l.log.Error("unable to get tx data", "err", err) return err
return
}
l.sendTransaction(txdata, queue, receiptsCh)
} }
l.sendTransaction(txdata, queue, receiptsCh)
return nil
} }
// sendTransaction creates & submits a transaction to the batch inbox address with the given `data`. // sendTransaction creates & submits a transaction to the batch inbox address with the given `data`.
......
...@@ -24,7 +24,6 @@ type Queue[T any] struct { ...@@ -24,7 +24,6 @@ type Queue[T any] struct {
txMgr TxManager txMgr TxManager
maxPending uint64 maxPending uint64
pendingChanged func(uint64) pendingChanged func(uint64)
receiptWg sync.WaitGroup
pending atomic.Uint64 pending atomic.Uint64
groupLock sync.Mutex groupLock sync.Mutex
groupCtx context.Context groupCtx context.Context
...@@ -50,7 +49,6 @@ func NewQueue[T any](ctx context.Context, txMgr TxManager, maxPending uint64, pe ...@@ -50,7 +49,6 @@ func NewQueue[T any](ctx context.Context, txMgr TxManager, maxPending uint64, pe
// Wait waits for all pending txs to complete (or fail). // Wait waits for all pending txs to complete (or fail).
func (q *Queue[T]) Wait() { func (q *Queue[T]) Wait() {
q.receiptWg.Wait()
if q.group == nil { if q.group == nil {
return return
} }
...@@ -61,9 +59,9 @@ func (q *Queue[T]) Wait() { ...@@ -61,9 +59,9 @@ func (q *Queue[T]) Wait() {
// and then send the next tx. // and then send the next tx.
// //
// The actual tx sending is non-blocking, with the receipt returned on the // The actual tx sending is non-blocking, with the receipt returned on the
// provided receipt channel. // provided receipt channel .If the channel is unbuffered, the goroutine is
// blocked from completing until the channel is read from.
func (q *Queue[T]) Send(id T, candidate TxCandidate, receiptCh chan TxReceipt[T]) { func (q *Queue[T]) Send(id T, candidate TxCandidate, receiptCh chan TxReceipt[T]) {
q.receiptWg.Add(1)
group, ctx := q.groupContext() group, ctx := q.groupContext()
group.Go(func() error { group.Go(func() error {
return q.sendTx(ctx, id, candidate, receiptCh) return q.sendTx(ctx, id, candidate, receiptCh)
...@@ -77,18 +75,13 @@ func (q *Queue[T]) Send(id T, candidate TxCandidate, receiptCh chan TxReceipt[T] ...@@ -77,18 +75,13 @@ func (q *Queue[T]) Send(id T, candidate TxCandidate, receiptCh chan TxReceipt[T]
// transaction is queued and this method returns true. // transaction is queued and this method returns true.
// //
// The actual tx sending is non-blocking, with the receipt returned on the // The actual tx sending is non-blocking, with the receipt returned on the
// provided receipt channel. // provided receipt channel. If the channel is unbuffered, the goroutine is
// blocked from completing until the channel is read from.
func (q *Queue[T]) TrySend(id T, candidate TxCandidate, receiptCh chan TxReceipt[T]) bool { func (q *Queue[T]) TrySend(id T, candidate TxCandidate, receiptCh chan TxReceipt[T]) bool {
q.receiptWg.Add(1)
group, ctx := q.groupContext() group, ctx := q.groupContext()
started := group.TryGo(func() error { return group.TryGo(func() error {
return q.sendTx(ctx, id, candidate, receiptCh) return q.sendTx(ctx, id, candidate, receiptCh)
}) })
if !started {
// send didn't start so receipt will never be available
q.receiptWg.Done()
}
return started
} }
func (q *Queue[T]) sendTx(ctx context.Context, id T, candidate TxCandidate, receiptCh chan TxReceipt[T]) error { func (q *Queue[T]) sendTx(ctx context.Context, id T, candidate TxCandidate, receiptCh chan TxReceipt[T]) error {
...@@ -97,15 +90,11 @@ func (q *Queue[T]) sendTx(ctx context.Context, id T, candidate TxCandidate, rece ...@@ -97,15 +90,11 @@ func (q *Queue[T]) sendTx(ctx context.Context, id T, candidate TxCandidate, rece
q.pendingChanged(q.pending.Add(^uint64(0))) // -1 q.pendingChanged(q.pending.Add(^uint64(0))) // -1
}() }()
receipt, err := q.txMgr.Send(ctx, candidate) receipt, err := q.txMgr.Send(ctx, candidate)
go func() { receiptCh <- TxReceipt[T]{
// notify from a goroutine to ensure the receipt channel won't block method completion ID: id,
receiptCh <- TxReceipt[T]{ Receipt: receipt,
ID: id, Err: err,
Receipt: receipt, }
Err: err,
}
q.receiptWg.Done()
}()
return err return err
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment