If you want a single step increasing number, you should store the max_value_added on the btree itself. This will make sure that you'll have to recompute the new max_value_added as the transaction fails if max_value_added was modified.
ie.
moment in time: now; now+1; now+2; now+3; now+4; now+5
transaction a: start; compute max_value = 2; wait; commit; failed; retry request
transaction b: wait; start; compute max_value = 2; commit; committed; done
At now+5
transaction will have to recompute it's max_value. It will do so, because of transaction retry.
The retry will read the updates max_value_added from the container object (2) and computes max_value_added = 3.
Which is why I think you we should store a counter (max_value_added
) on container._tree
, which needs to updated when adding an object to the container. Updating can be done using a objectAdded
event or custom __setitem__
in a class for your container.
An example could be:
class SomeStorage(dict):
pass
class SomeContainer:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._tree = SomeStorage()
self._tree.max_numeric_id_seen = -1
def __setitem__(self, key: str, value):
try:
self._tree.max_numeric_id_seen = max(int(key), self._tree.max_numeric_id_seen)
except ValueError:
pass
self._tree[key] = value
class NumericIdChooser:
def __init__(self, context):
self.context = context
# Grab from registry, or from a content field, or provide via an adapter
self.min_numeric_id = 0
def checkName(self, name:str, ob=None):
try:
name_number = int(name)
current_max: int = self.context._tree.max_numeric_id_seen
return name_number > current_max and name_number >= self.min_numeric_id
except ValueError:
return name not in self.context._tree
def chooseName(self, name, ob=None)-> str:
try:
chosen = max(self.min_numeric_id, self.context._tree.max_numeric_id_seen + 1, int(name))
except ValueError:
chosen = name
chosen = str(chosen)
if self.checkName(chosen, ob):
return chosen
raise RuntimeError(f"Sound like a you-problem to me.")
Some test cases
container = SomeContainer()
chooser = NumericIdChooser(context=container)
def av(output: str):
print(output)
assert output.endswith(('=True', '= True')), f"Failed: {output}"
av(f"{chooser.checkName('0') is True=}")
av(f"{chooser.checkName('1') is True=}")
av(f"{chooser.checkName('3') is True=}")
av(f"{chooser.checkName('henk') is True=}")
av(f"{chooser.checkName(1)=}")
av(f"{chooser.checkName(3)=}")
av(f"{chooser.chooseName('harf') == 'harf'=}")
av(f"{chooser.chooseName('999') == '999'=}")
av(f"{chooser.chooseName('-1') == '0'=}")
av(f"{chooser.context._tree.max_numeric_id_seen == -1=}")
av(f"{container._tree == {}=}")
chooser.min_numeric_id = 44
av(f"{chooser.checkName('0') is False=}")
av(f"{chooser.checkName('1') is False=}")
av(f"{chooser.checkName('3') is False=}")
av(f"{chooser.checkName('henk') is True=}")
av(f"{chooser.checkName(1) is False=}")
av(f"{chooser.checkName(3) is False=}")
av(f"{chooser.chooseName('harf') == 'harf'=}")
av(f"{chooser.chooseName('999') == '999'=}")
av(f"{chooser.chooseName('-1') == '44'=}")
av(f"{chooser.context._tree.max_numeric_id_seen == -1=}")
av(f"{container._tree == {}=}")
container['henk'] = 'I exist'
container['0'] = 'I exist 0'
container['3'] = 'I exist 3'
container['1'] = 'I exist 1'
av(f"{chooser.context._tree.max_numeric_id_seen == 3=}")
av(f"{list(chooser.context._tree.keys()) == ['henk', '0', '3', '1']=}")
# This is allowed according to the container, but the namechooser enforces the min value for the keys.
container[-1] = 'I exist -1'
av(f"{chooser.context._tree.max_numeric_id_seen == 3=}")
av(f"{list(chooser.context._tree.keys()) == ['henk', '0', '3', '1', -1]=}")
The starting number is a bit finicky.
Having typed all this, it might be easier to maintain as a behaviour
like id from title
, where you apply a marker interface to either all content types that need this or the instances via local behaviors.