Sharing objects between workers

botogram’s runner is fast because it’s able to process multiple messages at once, and to archive this result botogram spawns multiple processes, called “workers”. The problem with this is, you can’t easily share objects between the workers, because each one lives in a different process, within a different memory space.

The solution to this problem is shared memory. Shared memory allows you to store global state without worrying about synchronizing it. It just works as a standard Python dictionary. Also, each component has a different shared memory than your bot, so you don’t need to worry about conflicts between components.

How to use shared memory

If your function requires the shared argument, botogram will fill it with your bot’s shared memory object, which has the same API as the builtin dict. Then, you can store in it everything you want, and it will be synchronized between the processes.

Please note that the shared memory object is provided only if the function is called by botogram itself: if you call it directly, that argument won’t be provided.

Note

Synchronization uses pickle under the hood, so you can store in the shared memory only objects pickle knows how to serialize. Please refer to the official Python documentation for more information about this.

Here there is a simple example of an hook which uses the shared memory to count how much messages has been sent:

@bot.process_message
def increment(shared, chat, message):
    if "messages" not in shared:
        shared["messages"] = 0

    if message.text is None:
        return
    shared["messages"] += 1

As you can see, first of all the code initializes the messages key if it doesn’t exist yet. Then it just increments it. Next there is an example of a command which displays the current messages count calculated by the hook above:

@bot.command("count")
def count(shared, chat, message, args):
    messages = 0
    if "messages" in shared:
        messages = shared["messages"]

    chat.send("This bot received %s messages" % shared["messages"])

Shared memory preparers

In the example above, a big part of the code is just to handle the case when the shared memory doesn’t contain the count key, and that’s possible only at startup. In order to solve this problem, you can use the prepare_memory() decorator.

Functions decorated with that decorator will be called only the first time you require the shared memory. This means you can use them to set the initial value of all the keys you want to use in the shared memory.

For example, let’s refactor the code above to use a preparer:

@bot.prepare_memory
def prepare_memory(shared):
    shared["messages"] = 0

@bot.process_message
def increment(shared, chat, message):
    if message.text is None:
        return
    shared["messages"] += 1

@bot.command("count")
def count_command(shared, chat, message, args):
    chat.send("This bot received %s messages" % shared["messages"])

As you can see, the code is now clearer, and we can be sure the key we need will always exist. This can especially be useful if you have a lot more hooks.

Shared memory in components

Shared memory is really useful while you’re developing components, because it’s unique both to your component and the current bot. This means, you don’t have to worry about naming conflicts with other components, and each bot’s data will be isolated from each other if the component is used by multiple bots.

Using shared memory within a component is the same as using it in your bot’s main code: just require the shared argument to your component’s function and botogram will make sure it receives the component’s shared memories. To add a shared memory preparer, you can instead provide the function to the add_memory_preparer() method.

Dealing with concurrency issues with locks

Normally you don’t need to worry about concurrency issues in botogram: everything is local to your process, and you can’t interact with the other ones. But when you start dealing with shared memory this isn’t true anymore, because two processes can write to the same key at the same time.

If you need to protect yourself from concurrency issues, shared memory’s locks are the way to go. They’ve the same API as the Python native ones, but they’re also customized to fit better in botogram.

In order to use locks you can call the lock method on a shared memories object, providing to it the name of the lock. Then you can use it as a context manager in order to lock specific parts of your code:

@bot.command("count")
def count_command(shared, chat):
    """Send the number of messages sent in this chat"""
    chat.send("Number of messages: %s" % shared["messages"][chat.id])

@bot.process_message
def increment(shared, chat):
    """Example command for locks"""
    with shared.lock("update-messages"):
        messages = shared["messages"]
        messages[chat.id] += 1
        shared["messages"] = messages

Remember that lock names are unique to your bot/component, so you don’t need to worry about naming conflicts.