Ringbuffer queue
For those dealing with a little out of the ordinary data structures.
#
GoalBuild a transient storage adapter for a FIFO (First-in-first-out) queue.
#
Use casesHandling complex data structures stored in storage.
#
OverviewA ringbuffer that abstracts over storage can be a useful tool when handling storage migrations for more sophisticated pallets. This guide is intended to step you through how to build a storage adapter and use it for a FIFO queue. It will guide you through building a function to overwrite old storage values within pre-defined bounds.
#
Steps#
1. Defining the RingBuffer traitThe RingBuffer
trait will serve as the interface to our queue. It must define:
commit
: to sync the changes made to the underlying storage.push
: to push an item onto the end of the queuepop
: to pop an item from the start of a queueis_empty
: checks if queue is empty
Define it as shown below:
#
2. Specifying the Ringbuffer transient#
Start and End boundsWe will be storing the start and end of the ringbuffer separately from the actual items and will thus need to store these in our struct:
#
Defining storage interface boundsIn order to access the underlying storage we need to include the bounds in storage that can be accessed.
Let type B
correspond to the specified bounds; M
to the item storage; and Item
to specify the constraints on M
. Write this as follows:
note
The Query
type is specified to help with type inference (because the value returned can
be different from the stored representation).
The Codec
and EncodeLike
type constraints make sure that both items and indices can be stored in storage.
The PhantomData
is needed in order
to "hold on to" the types during the lifetime of the transient object.
Index
#
Specifying type constraints for Specify the default type for Index
as u16
. In addition, add `WrappingsOps
and From<u8>
.
#
3. RingBuffer implementationNow that we have the type definition for RingBufferTransient
we need to write the implementation.
#
Initialize the transientSpecify how to create a new instance by creating a function called new
, which makes use of the bounds B
in storage to intialize the transient:
RingBufferTrait
#
Implementing To implement RingBufferTrait
, write the following functions:
commit()
: to put the potentially changed bounds in storageis_empty()
: to check whether the queue is empty to avoid expensive accesses to storagepush()
: to uphold the corresponding invariants fromis_empty()
.pop()
: if the queue is not empty wetake
the value atself.start
from storage, then incrementself.start
to point to the new first item of the queuewrapping_add
: allows our ringbuffer to wrap around when reachingmax_value
of theIndex
type. The next step covers writing theWrappingOps
trait declaration.
The if
is necessary because we need to keep the invariant that start == end
means that the queue
is empty, otherwise we would need to keep track of this state separately. Consequently, we "toss away" the
oldest item in the queue (if a new item is pushed into a full queue) by incrementing the start index.
WrappingOps
trait#
The need for the Since std
does not provide a trait that allows the ringbuffer to be agnostic to the concrete Index
type used. Therefore, we need to create our own trait for the types we want to support (u8
, u16
, u32
and u64
):
Drop
trait#
Implementing the In order to make the usage more ergonomic and to avoid synchronization errors (where the storage map
diverges from the bounds) we also implement the
Drop
trait:
On drop
, we commit
the bounds to storage. With this implementation of Drop
, commit
is called
when our transient goes out of scope, making sure that the storage state is consistent for the next
call to the using pallet.
#
Examplesringbuffer-queue/src/lib.rs
: Shows a typical usage of the transient storage adapter.ringbuffer-queue/src/ringbuffer.rs
: Contains an implementation of a transient storage adapter.