RC: W10 D5 — Creating a monotonically incrementing sequence number
April 19, 2024Today, I wanted to add a sequence number to each entry written to my engine.
Those sequence numbers will later be used to create snapshots of the database (i.e. a stable view of the database at a
particular point in time) and thus need to be monotonically increasing.
So I implemented a SequenceNumberGenerator
to yield values incrementing by 1 every time it is called.
To add the sequence number to each record, I thus did the following:
class Record:
def __init__(self, key, value):
self.key = key
self.value = value
self.sequence_number = next(SequenceNumberGenerator())
However, with this method, the generator is re-instantiated with every new record and thus, every record is assigned the
same sequence number: 0.
One way to solve the problem would have been to have a single generator instantiated at the top level (the LsmStorage
class) and passed down at every call of the Record
class (and all intermediary classes), but that would have been
messy and error-prone.
Instead, I decided to use the singleton pattern to ensure that only
one instance of the generator was created and reused for every need.
I decided to create a decorator that would encapsulate the singleton.
A decorator is simply a function that is applied to the class or function that it “decorates” (SequenceNumberGenerator
in the example below), and that does it with a particular syntax: when @decorator
is written above a class or
function, the decorator
function is applied to that class or function. Therefore,
writing @decorator; class Decorated
is equivalent to doing decorator(Decorated)
.
Implementing the singleton pattern is relatively easy: I proceeded by creating a dictionary of class instances and, if
the class instantiated is already in the dictionary, I use it (thereby creating only a single instance).
Here is what I came up with:
def singleton(cls):
instances = {}
def wrapper(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return wrapper
@singleton
class SequenceNumberGenerator:
def __init__(self, start=0):
self.current = start
def __iter__(self):
return self
def __next__(self):
current = self.current
self.current += 1
return current
This worked well: if two generators are instantiated, their value keeps is monotonically increasing, as expected:
def test_sequence_number_generator_is_a_singleton():
# GIVEN
generator1 = SequenceNumberGenerator()
assert next(generator1) == 0
assert next(generator1) == 1
generator2 = SequenceNumberGenerator()
# WHEN/THEN
assert next(generator2) == 2
assert next(generator1) == 3
I think I could definitely have stopped there.
However, I was a little troubled with this behavior for my tests: basically, since I made it a singleton, all my tests
used the same instance and thus, the exact sequence number for a given test would depend on the ordering of tests (if I
add one additional test that creates a record, then the values for all subsequent tests are shifted by 1).
I really want my tests to be independent of one another (mostly because it would not be very fun to have to update the
sequence number of the tests asserting it every time I added, deleted or modified another one).
Therefore, I thought of adding a reset
method on my singleton that would be called before each test.
To do this, I needed to use a slightly more complex syntax: I wrapped the logic inside
a SingletonWrapper
class inheriting from the decorated class (SingleNumberGenerator
).
I also provided it with a __new__
method to handle instantiation and with a reset
method to handle resetting the
generator.
Hereunder is the updated implementation I came up with:
def singleton(decorated_class):
instances = {}
class SingletonWrapper(decorated_class):
def __new__(cls, *args, **kwargs):
if decorated_class not in instances:
instances[decorated_class] = super().__new__(decorated_class)
instances[decorated_class].__init__(*args, **kwargs)
return instances[decorated_class]
@classmethod
def reset(cls):
if decorated_class in instances:
del instances[decorated_class]
return SingletonWrapper
@singleton
class SingleNumberGenerator:
...
In this code, SingletonWrapper
inherits from decorated_class
.
Since the @singleton
decorator is applied to SingleNumberGenerator
, decorated_class
in this case corresponds
to SingleNumberGenerator
.
In other words, when instantiating SingleNumberGenerator
, we obtain a regular SingleNumberGenerator
object
incremented with the reset
method defined in the SingletonWrapper
class.
The reset
method simply deletes the instance from the dictionary of available instances.
As for the __new__
method, it does the same as the previous implementation: namely returning the already existing
instance or creating a new one the first time.
When creating a new instance, it first calls the creator of the decorated class
(with super().__new__(decorated_class)
) and then initialize the instance with the correct arguments
(with instances[decorated_class].__init__(*args, **kwargs)
).
The behavior of the reset
method can then be tested as follows:
def test_sequence_number_generator_can_be_restarted():
SequenceNumberGenerator.reset()
# GIVEN
generator1 = SequenceNumberGenerator()
assert next(generator1) == 0
assert next(generator1) == 1
assert next(generator1) == 2
# WHEN
generator2 = SequenceNumberGenerator()
SequenceNumberGenerator.reset()
generator3 = SequenceNumberGenerator()
# THEN
assert next(generator1) == 3
assert next(generator2) == 4
assert next(generator3) == 0
As expected, generator1
and generator2
that were instantiated before the reset carry on incrementing, whereas
generator3
which was instantiated after the reset start from 0.
Victory! 🎉