Commit e9f8ae50 authored by Brian Bland's avatar Brian Bland

Improve select logic, avoid double-closure

parent 995e3e42
...@@ -308,8 +308,8 @@ func (c *channelBuilder) IsFull() bool { ...@@ -308,8 +308,8 @@ func (c *channelBuilder) IsFull() bool {
// FullErr returns the reason why the channel is full. If not full yet, it // FullErr returns the reason why the channel is full. If not full yet, it
// returns nil. // returns nil.
// //
// It returns a ChannelFullError wrapping one of six possible reasons for the // It returns a ChannelFullError wrapping one of the following possible reasons
// channel being full: // for the channel being full:
// - ErrInputTargetReached if the target amount of input data has been reached, // - ErrInputTargetReached if the target amount of input data has been reached,
// - derive.MaxRLPBytesPerChannel if the general maximum amount of input data // - derive.MaxRLPBytesPerChannel if the general maximum amount of input data
// would have been exceeded by the latest AddBlock call, // would have been exceeded by the latest AddBlock call,
...@@ -405,12 +405,11 @@ func (c *channelBuilder) outputFrame() error { ...@@ -405,12 +405,11 @@ func (c *channelBuilder) outputFrame() error {
} }
// Close immediately marks the channel as full with an ErrTerminated // Close immediately marks the channel as full with an ErrTerminated
// if the channel is not already full, then outputs any remaining frames. // if the channel is not already full.
func (c *channelBuilder) Close() error { func (c *channelBuilder) Close() {
if !c.IsFull() { if !c.IsFull() {
c.setFullErr(ErrTerminated) c.setFullErr(ErrTerminated)
} }
return c.closeAndOutputAllFrames()
} }
// HasFrame returns whether there's any available frame. If true, it can be // HasFrame returns whether there's any available frame. If true, it can be
......
...@@ -81,12 +81,9 @@ func (s *channelManager) TxFailed(id txID) { ...@@ -81,12 +81,9 @@ func (s *channelManager) TxFailed(id txID) {
} }
s.metr.RecordBatchTxFailed() s.metr.RecordBatchTxFailed()
// If this channel has no submitted transactions, put the pending blocks back into the if s.closed && len(s.confirmedTransactions) == 0 && len(s.pendingTransactions) == 0 {
// local saved blocks and reset this state so it can try to build a new channel. s.log.Info("Channel has no submitted transactions, clearing for shutdown", "chID", s.pendingChannel.ID())
if len(s.confirmedTransactions) == 0 && len(s.pendingTransactions) == 0 { s.Clear()
s.log.Info("Channel has no submitted transactions", "chID", s.pendingChannel.ID())
s.blocks = append(s.pendingChannel.Blocks(), s.blocks...)
s.clearPendingChannel()
} }
} }
...@@ -194,11 +191,6 @@ func (s *channelManager) TxData(l1Head eth.BlockID) (txData, error) { ...@@ -194,11 +191,6 @@ func (s *channelManager) TxData(l1Head eth.BlockID) (txData, error) {
return s.nextTxData() return s.nextTxData()
} }
// Avoid producing new frames if the channel has been explicitly closed.
if s.closed {
return txData{}, io.EOF
}
// No pending frame, so we have to add new blocks to the channel // No pending frame, so we have to add new blocks to the channel
// If we have no saved blocks, we will not be able to create valid frames // If we have no saved blocks, we will not be able to create valid frames
...@@ -210,8 +202,11 @@ func (s *channelManager) TxData(l1Head eth.BlockID) (txData, error) { ...@@ -210,8 +202,11 @@ func (s *channelManager) TxData(l1Head eth.BlockID) (txData, error) {
return txData{}, err return txData{}, err
} }
if err := s.processBlocks(); err != nil { // Avoid processing blocks if the channel manager has been explicitly closed.
return txData{}, err if !s.closed {
if err := s.processBlocks(); err != nil {
return txData{}, err
}
} }
// Register current L1 head only after all pending blocks have been // Register current L1 head only after all pending blocks have been
...@@ -365,9 +360,21 @@ func l2BlockRefFromBlockAndL1Info(block *types.Block, l1info derive.L1BlockInfo) ...@@ -365,9 +360,21 @@ func l2BlockRefFromBlockAndL1Info(block *types.Block, l1info derive.L1BlockInfo)
// This ensures that no new frames will be produced, but there may be any number // This ensures that no new frames will be produced, but there may be any number
// of pending frames produced before this call which should still be published. // of pending frames produced before this call which should still be published.
func (s *channelManager) Close() error { func (s *channelManager) Close() error {
if s.closed {
return nil
}
s.closed = true s.closed = true
// Any pending state can be proactively cleared if there are no submitted transactions
if len(s.confirmedTransactions) == 0 && len(s.pendingTransactions) == 0 {
s.Clear()
}
if s.pendingChannel == nil { if s.pendingChannel == nil {
return nil return nil
} }
return s.pendingChannel.Close()
s.pendingChannel.Close()
return nil
} }
...@@ -332,11 +332,9 @@ func TestChannelManager_TxResend(t *testing.T) { ...@@ -332,11 +332,9 @@ func TestChannelManager_TxResend(t *testing.T) {
log := testlog.Logger(t, log.LvlError) log := testlog.Logger(t, log.LvlError)
m := NewChannelManager(log, metrics.NoopMetrics, m := NewChannelManager(log, metrics.NoopMetrics,
ChannelConfig{ ChannelConfig{
TargetNumFrames: 2, TargetFrameSize: 0,
TargetFrameSize: 1000, MaxFrameSize: 120_000,
MaxFrameSize: 2000,
ApproxComprRatio: 1.0, ApproxComprRatio: 1.0,
ChannelTimeout: 1000,
}) })
a, _ := derivetest.RandomL2Block(rng, 4) a, _ := derivetest.RandomL2Block(rng, 4)
...@@ -345,30 +343,24 @@ func TestChannelManager_TxResend(t *testing.T) { ...@@ -345,30 +343,24 @@ func TestChannelManager_TxResend(t *testing.T) {
txdata0, err := m.TxData(eth.BlockID{}) txdata0, err := m.TxData(eth.BlockID{})
require.NoError(err) require.NoError(err)
txdata0bytes := txdata0.Bytes()
// confirm one frame to keep the channel open data0 := make([]byte, len(txdata0bytes))
m.TxConfirmed(txdata0.ID(), eth.BlockID{})
txdata1, err := m.TxData(eth.BlockID{})
require.NoError(err)
txdata1bytes := txdata1.Bytes()
data1 := make([]byte, len(txdata1bytes))
// make sure we have a clone for later comparison // make sure we have a clone for later comparison
copy(data1, txdata1bytes) copy(data0, txdata0bytes)
// ensure channel is drained // ensure channel is drained
_, err = m.TxData(eth.BlockID{}) _, err = m.TxData(eth.BlockID{})
require.ErrorIs(err, io.EOF) require.ErrorIs(err, io.EOF)
// requeue frame // requeue frame
m.TxFailed(txdata1.ID()) m.TxFailed(txdata0.ID())
txdata2, err := m.TxData(eth.BlockID{}) txdata1, err := m.TxData(eth.BlockID{})
require.NoError(err) require.NoError(err)
data2 := txdata2.Bytes() data1 := txdata1.Bytes()
require.Equal(data2, data1) require.Equal(data1, data0)
fs, err := derive.ParseFrames(data2) fs, err := derive.ParseFrames(data1)
require.NoError(err) require.NoError(err)
require.Len(fs, 1) require.Len(fs, 1)
} }
...@@ -457,13 +449,13 @@ func TestChannelManagerCloseNoPendingChannel(t *testing.T) { ...@@ -457,13 +449,13 @@ func TestChannelManagerCloseNoPendingChannel(t *testing.T) {
// can gracefully close with a pending channel, and will not produce any // can gracefully close with a pending channel, and will not produce any
// new channel frames after this point. // new channel frames after this point.
func TestChannelManagerClosePendingChannel(t *testing.T) { func TestChannelManagerClosePendingChannel(t *testing.T) {
rng := rand.New(rand.NewSource(time.Now().UnixNano())) rng := rand.New(rand.NewSource(1))
log := testlog.Logger(t, log.LvlCrit) log := testlog.Logger(t, log.LvlCrit)
m := NewChannelManager(log, metrics.NoopMetrics, m := NewChannelManager(log, metrics.NoopMetrics,
ChannelConfig{ ChannelConfig{
TargetNumFrames: 100, TargetNumFrames: 100,
TargetFrameSize: 25_000, TargetFrameSize: 20_000,
MaxFrameSize: 40_000, MaxFrameSize: 20_000,
ApproxComprRatio: 1.0, ApproxComprRatio: 1.0,
ChannelTimeout: 1000, ChannelTimeout: 1000,
}) })
...@@ -505,7 +497,7 @@ func TestChannelManagerClosePendingChannel(t *testing.T) { ...@@ -505,7 +497,7 @@ func TestChannelManagerClosePendingChannel(t *testing.T) {
// can gracefully close after producing transaction frames if none of these // can gracefully close after producing transaction frames if none of these
// have successfully landed on chain. // have successfully landed on chain.
func TestChannelManagerCloseAllTxsFailed(t *testing.T) { func TestChannelManagerCloseAllTxsFailed(t *testing.T) {
rng := rand.New(rand.NewSource(time.Now().UnixNano())) rng := rand.New(rand.NewSource(1))
log := testlog.Logger(t, log.LvlCrit) log := testlog.Logger(t, log.LvlCrit)
m := NewChannelManager(log, metrics.NoopMetrics, m := NewChannelManager(log, metrics.NoopMetrics,
ChannelConfig{ ChannelConfig{
......
...@@ -293,9 +293,18 @@ func (l *BatchSubmitter) loop() { ...@@ -293,9 +293,18 @@ func (l *BatchSubmitter) loop() {
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
// prioritize the `done` condition over the ticker, even though select ordering is randomized
select {
case <-l.done:
l.publishStateToL1(ctx)
return
default:
}
l.loadBlocksIntoState(l.ctx) l.loadBlocksIntoState(l.ctx)
l.publishStateToL1(ctx) l.publishStateToL1(ctx)
case <-l.done: case <-l.done:
l.publishStateToL1(ctx)
return return
} }
} }
...@@ -305,6 +314,14 @@ func (l *BatchSubmitter) loop() { ...@@ -305,6 +314,14 @@ func (l *BatchSubmitter) loop() {
// submits the associated data to the L1 in the form of channel frames. // submits the associated data to the L1 in the form of channel frames.
func (l *BatchSubmitter) publishStateToL1(ctx context.Context) { func (l *BatchSubmitter) publishStateToL1(ctx context.Context) {
for { for {
// Attempt to gracefully terminate the current channel, ensuring that no new frames will be
// produced. Any remaining frames must still be published to the L1 to prevent stalling.
select {
case <-l.done:
l.state.Close()
default:
}
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)
...@@ -327,14 +344,6 @@ func (l *BatchSubmitter) publishStateToL1(ctx context.Context) { ...@@ -327,14 +344,6 @@ func (l *BatchSubmitter) publishStateToL1(ctx context.Context) {
} else { } else {
l.recordConfirmedTx(txdata.ID(), receipt) l.recordConfirmedTx(txdata.ID(), receipt)
} }
// Attempt to gracefully terminate the current channel, ensuring that no new frames will be
// produced. Any remaining frames must still be published to the L1 to prevent stalling.
select {
case <-l.done:
l.state.Close()
default:
}
} }
} }
......
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