Execute statement only once in Python

Question:

I’m running a periodic task on Celery that executes the same code once every 3 minutes. If a condition is True, an action is performed (a message is sent), but I need that message to be sent only once.

The ideal would be that that message, if sent, it could not be sent in the next 24 hours (even though the function will keep being executed every 3 minutes), and after those 24 hours, the condition will be checked again and message sent again if still True. How can I accomplish that with Python? I put here some code:

if object.shussui:
    client.conversation_start({
        'channelId': 'x',
        'to': 'user',
        'type': 'text',
        'content': {
            'text': 'body message')
        }
    })

Here the condition is being checked, and if shussui is True the client.start_conversation sends a message.

Asked By: Jim

||

Answers:

You could keep the last time the message was sent and check that 24 hours have passed:

from time import time
lastSent = None
if object.shussui and (not lastSent or (time() - lastSent) >= 24 * 60 * 60):
    client.conversation_start({
        'channelId': 'x',
        'to': 'user',
        'type': 'text',
        'content': {
            'text': 'body message')
        }
    })
    lastSent = time()
Answered By: Mureinik

Apart from adding a check for time of last execution, as done in Mureinik’s answer, if you have multiple workers, you should get and store the last execution time in a central shared location. For example, whichever message broker you’re using with celery, or to a database.

The key for last_sent should also include a deterministic signature to match unique messages and/or user ids; or whatever criteria you need. With the value being a datetime object or an int/float for seconds (for time.time())

If you don’t store this in a central/shared location, then when a different worker/instance gets the next task for the same message, it would end up getting resent immediately. Because the previous last_sent would be stored only on the previous worker’s memory and the current worker would have not have that updated value.

Examples of how you could store the last_sent info:

  • In Redis, the keys could be in the form last_sent:user:1234:msg:9876 with the value as a datetime.datetime or time.time. Here 9876 is the hash for the message, like hash('hello'). Don’t store the message itself. [works for multiple worker instances]

    import redis
    
    client = redis.Redis()
    key = 'last_sent:user:1234:msg:9876'
    last_sent = client.get(key) or '0'  # missing/None will be converted to 0
    if object.shussui and time.time() - int(last_sent) > 24 * 60 * 60:
        last_sent = client.set(key, time.time())
        client.conversation_start({...})
    

    It’s possible to do this as an atomic operation which avoids race-conditions. Use set with a 24 hour expiration since messages would need to be sent after that period or if it’s not present Redis. This can be used to just store some arbitrary string and use its existence in redis as a flag:

    ONE_DAY = 24 * 60 * 60
    key = 'last_sent:user:1234:msg:9876'
    
    # set a new flag only if it doesn't exist, with a 24-hour expiration
    status = client.set(key, 'A', nx=True, ex=ONE_DAY)
    
    # None implies it was present in redis so 24 hours have not yet passed
    if object.shussui and status is not None:
        client.conversation_start({...})
    

    The if object.shussui and status is not None can be simplified to if object.shussui and status.

  • In a DB, it could be a table last_sent with user_id, msg_hash, sent_at. [works for multiple worker instances]

  • In a dictionary with the user_id and the msg hash. [only works for single worker instances since this is stored in memory]:

    last_sent = {
        (1234, msg1): ..., # <datetime or time>
        (1234, msg2): ..., # another message
        (5678, msg1): ..., # another user, same message
        ... # etc.
    }
    

Also, based on your needs and situation, you need to decide how you want to handle race conditions. Like same message triggers twice in a row and before the first one has updated ‘last_sent’.

Answered By: aneroid
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.