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.
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
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
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.