For those dealing with a little out of the ordinary data structures.
Build a transient storage adapter for a FIFO (First-in-first-out) queue.
Handling complex data structures stored in storage.
A 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.
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 queue
pop: to pop an item from the start of a queue
is_empty: checks if queue is empty
Define it as shown below:
We 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:
In order to access the underlying storage we need to include the bounds in storage that can be accessed.
B correspond to the specified bounds;
M to the item storage; and
Item to specify the constraints on
M. Write this as follows:
Query type is specified to help with type inference (because the value returned can
be different from the stored representation).
type constraints make sure that both items and indices can be stored in storage.
PhantomData is needed in order
to "hold on to" the types during the lifetime of the transient object.
Specifying type constraints for
Specify the default type for
u16. In addition, add `
Now that we have the type definition for
RingBufferTransient we need to write the implementation.
Specify 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, write the following functions:
commit(): to put the potentially changed bounds in storage
is_empty(): to check whether the queue is empty to avoid expensive accesses to storage
push(): to uphold the corresponding invariants from
pop(): if the queue is not empty we
takethe value at
self.startfrom storage, then increment
self.startto point to the new first item of the queue
wrapping_add: allows our ringbuffer to wrap around when reaching
Indextype. The next step covers writing 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.
The need for the
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 (
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
commit the bounds to storage. With this implementation of
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.
ringbuffer-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.