@@ -2,9 +2,11 @@ package single
22
33import (
44 "context"
5+ "encoding/binary"
56 "encoding/hex"
67 "errors"
78 "fmt"
9+ "strconv"
810 "sync"
911
1012 ds "github.com/ipfs/go-datastore"
@@ -20,30 +22,56 @@ import (
2022// ErrQueueFull is returned when the batch queue has reached its maximum size
2123var ErrQueueFull = errors .New ("batch queue is full" )
2224
25+ // initialSeqNum is the starting sequence number for new queues.
26+ // It is set to the middle of the uint64 range to allow for both
27+ // appending (incrementing) and prepending (decrementing) transactions.
28+ const initialSeqNum = uint64 (0x8000000000000000 )
29+
2330func newPrefixKV (kvStore ds.Batching , prefix string ) ds.Batching {
2431 return ktds .Wrap (kvStore , ktds.PrefixTransform {Prefix : ds .NewKey (prefix )})
2532}
2633
34+ // queuedItem holds a batch and its associated persistence key
35+ type queuedItem struct {
36+ Batch coresequencer.Batch
37+ Key string
38+ }
39+
2740// BatchQueue implements a persistent queue for transaction batches
2841type BatchQueue struct {
29- queue []coresequencer. Batch
42+ queue []queuedItem
3043 head int // index of the first element in the queue
3144 maxQueueSize int // maximum number of batches allowed in queue (0 = unlimited)
32- mu sync.Mutex
33- db ds.Batching
45+
46+ // Sequence numbers for generating new keys
47+ nextAddSeq uint64
48+ nextPrependSeq uint64
49+
50+ mu sync.Mutex
51+ db ds.Batching
3452}
3553
3654// NewBatchQueue creates a new BatchQueue with the specified maximum size.
3755// If maxSize is 0, the queue will be unlimited.
3856func NewBatchQueue (db ds.Batching , prefix string , maxSize int ) * BatchQueue {
3957 return & BatchQueue {
40- queue : make ([]coresequencer.Batch , 0 ),
41- head : 0 ,
42- maxQueueSize : maxSize ,
43- db : newPrefixKV (db , prefix ),
58+ queue : make ([]queuedItem , 0 ),
59+ head : 0 ,
60+ maxQueueSize : maxSize ,
61+ db : newPrefixKV (db , prefix ),
62+ nextAddSeq : initialSeqNum ,
63+ nextPrependSeq : initialSeqNum - 1 ,
4464 }
4565}
4666
67+ // seqToKey converts a sequence number to a hex-encoded big-endian key.
68+ // We use big-endian so that lexicographical sort order matches numeric order.
69+ func seqToKey (seq uint64 ) string {
70+ b := make ([]byte , 8 )
71+ binary .BigEndian .PutUint64 (b , seq )
72+ return hex .EncodeToString (b )
73+ }
74+
4775// AddBatch adds a new transaction to the queue and writes it to the WAL.
4876// Returns ErrQueueFull if the queue has reached its maximum size.
4977func (bq * BatchQueue ) AddBatch (ctx context.Context , batch coresequencer.Batch ) error {
@@ -57,12 +85,14 @@ func (bq *BatchQueue) AddBatch(ctx context.Context, batch coresequencer.Batch) e
5785 return ErrQueueFull
5886 }
5987
60- if err := bq .persistBatch (ctx , batch ); err != nil {
88+ key := seqToKey (bq .nextAddSeq )
89+ if err := bq .persistBatch (ctx , batch , key ); err != nil {
6190 return err
6291 }
92+ bq .nextAddSeq ++
6393
6494 // Then add to in-memory queue
65- bq .queue = append (bq .queue , batch )
95+ bq .queue = append (bq .queue , queuedItem { Batch : batch , Key : key } )
6696
6797 return nil
6898}
@@ -72,25 +102,27 @@ func (bq *BatchQueue) AddBatch(ctx context.Context, batch coresequencer.Batch) e
72102// The batch is persisted to the DB to ensure durability in case of crashes.
73103//
74104// NOTE: Prepend intentionally bypasses the maxQueueSize limit to ensure high-priority
75- // transactions can always be re-queued. This means the effective queue size may temporarily
76- // exceed the configured maximum when Prepend is used. This is by design to prevent loss
77- // of transactions that have already been accepted but couldn't fit in the current batch.
105+ // transactions can always be re-queued.
78106func (bq * BatchQueue ) Prepend (ctx context.Context , batch coresequencer.Batch ) error {
79107 bq .mu .Lock ()
80108 defer bq .mu .Unlock ()
81109
82- if err := bq .persistBatch (ctx , batch ); err != nil {
110+ key := seqToKey (bq .nextPrependSeq )
111+ if err := bq .persistBatch (ctx , batch , key ); err != nil {
83112 return err
84113 }
114+ bq .nextPrependSeq --
115+
116+ item := queuedItem {Batch : batch , Key : key }
85117
86118 // Then add to in-memory queue
87119 // If we have room before head, use it
88120 if bq .head > 0 {
89121 bq .head --
90- bq .queue [bq .head ] = batch
122+ bq .queue [bq .head ] = item
91123 } else {
92124 // Need to expand the queue at the front
93- bq .queue = append ([]coresequencer. Batch { batch }, bq .queue ... )
125+ bq .queue = append ([]queuedItem { item }, bq .queue ... )
94126 }
95127
96128 return nil
@@ -106,8 +138,9 @@ func (bq *BatchQueue) Next(ctx context.Context) (*coresequencer.Batch, error) {
106138 return & coresequencer.Batch {Transactions : nil }, nil
107139 }
108140
109- batch := bq .queue [bq .head ]
110- bq .queue [bq .head ] = coresequencer.Batch {} // Release memory for the dequeued element
141+ item := bq .queue [bq .head ]
142+ // Release memory for the dequeued element
143+ bq .queue [bq .head ] = queuedItem {}
111144 bq .head ++
112145
113146 // Compact when head gets too large to prevent memory leaks
@@ -116,59 +149,72 @@ func (bq *BatchQueue) Next(ctx context.Context) (*coresequencer.Batch, error) {
116149 // frequent compactions on small queues
117150 if bq .head > len (bq .queue )/ 2 && bq .head > 100 {
118151 remaining := copy (bq .queue , bq .queue [bq .head :])
119- // Zero out the rest of the slice to release memory
152+ // Zero out the rest of the slice
120153 for i := remaining ; i < len (bq .queue ); i ++ {
121- bq .queue [i ] = coresequencer. Batch {}
154+ bq .queue [i ] = queuedItem {}
122155 }
123156 bq .queue = bq .queue [:remaining ]
124157 bq .head = 0
125158 }
126159
127- hash , err := batch .Hash ()
128- if err != nil {
129- return & coresequencer.Batch {Transactions : nil }, err
130- }
131- key := hex .EncodeToString (hash )
132-
133160 // Delete the batch from the WAL since it's been processed
134- err = bq . db . Delete ( ctx , ds . NewKey ( key ))
135- if err != nil {
161+ // Use the stored key directly
162+ if err := bq . db . Delete ( ctx , ds . NewKey ( item . Key )); err != nil {
136163 // Log the error but continue
137164 fmt .Printf ("Error deleting processed batch: %v\n " , err )
138165 }
139166
140- return & batch , nil
167+ return & item . Batch , nil
141168}
142169
143170// Load reloads all batches from WAL file into the in-memory queue after a crash or restart
144171func (bq * BatchQueue ) Load (ctx context.Context ) error {
145172 bq .mu .Lock ()
146173 defer bq .mu .Unlock ()
147174
148- // Clear the current queue
149- bq .queue = make ([]coresequencer. Batch , 0 )
175+ // Clear the current queue and reset sequences
176+ bq .queue = make ([]queuedItem , 0 )
150177 bq .head = 0
178+ bq .nextAddSeq = initialSeqNum
179+ bq .nextPrependSeq = initialSeqNum - 1
151180
152- q := query.Query {}
181+ q := query.Query {
182+ Orders : []query.Order {query.OrderByKey {}},
183+ }
153184 results , err := bq .db .Query (ctx , q )
154185 if err != nil {
155186 return fmt .Errorf ("error querying datastore: %w" , err )
156187 }
157188 defer results .Close ()
158189
159- // Load each batch
160190 for result := range results .Next () {
161191 if result .Error != nil {
162192 fmt .Printf ("Error reading entry from datastore: %v\n " , result .Error )
163193 continue
164194 }
195+ // We care about the last part of the key (the sequence number)
196+ // ds.Key usually has a leading slash.
197+ key := ds .NewKey (result .Key ).Name ()
198+
165199 pbBatch := & pb.Batch {}
166200 err := proto .Unmarshal (result .Value , pbBatch )
167201 if err != nil {
168- fmt .Printf ("Error decoding batch for key '%s': %v. Skipping entry.\n " , result . Key , err )
202+ fmt .Printf ("Error decoding batch for key '%s': %v. Skipping entry.\n " , key , err )
169203 continue
170204 }
171- bq .queue = append (bq .queue , coresequencer.Batch {Transactions : pbBatch .Txs })
205+
206+ batch := coresequencer.Batch {Transactions : pbBatch .Txs }
207+ bq .queue = append (bq .queue , queuedItem {Batch : batch , Key : key })
208+
209+ // Update sequences based on loaded keys to avoid collisions
210+ if seq , err := strconv .ParseUint (key , 16 , 64 ); err == nil {
211+ if seq >= bq .nextAddSeq {
212+ bq .nextAddSeq = seq + 1
213+ }
214+ if seq <= bq .nextPrependSeq {
215+ bq .nextPrependSeq = seq - 1
216+ }
217+ }
172218 }
173219
174220 return nil
@@ -182,14 +228,8 @@ func (bq *BatchQueue) Size() int {
182228 return len (bq .queue ) - bq .head
183229}
184230
185- // persistBatch persists a batch to the datastore
186- func (bq * BatchQueue ) persistBatch (ctx context.Context , batch coresequencer.Batch ) error {
187- hash , err := batch .Hash ()
188- if err != nil {
189- return err
190- }
191- key := hex .EncodeToString (hash )
192-
231+ // persistBatch persists a batch to the datastore with the given key
232+ func (bq * BatchQueue ) persistBatch (ctx context.Context , batch coresequencer.Batch , key string ) error {
193233 pbBatch := & pb.Batch {
194234 Txs : batch .Transactions ,
195235 }
@@ -199,7 +239,7 @@ func (bq *BatchQueue) persistBatch(ctx context.Context, batch coresequencer.Batc
199239 return err
200240 }
201241
202- // First write to DB for durability
242+ // Write to DB
203243 if err := bq .db .Put (ctx , ds .NewKey (key ), encodedBatch ); err != nil {
204244 return err
205245 }
0 commit comments