2

Or in other words, how to create a time delayed function?
I have a python bot that is supposed to send notifications to user's followers upon the usage of certain commands.

For example , if Tim runs the command ' >follow Tom ', all of Tim's followers will be notified in PM's that he followed Tom , and Tom will be notified that Tim followed him.

I have tested this function with a large amount of followers , and the bot remains stable and avoids being kicked from the server , i'm guessing because the for loop adds a delay to each message sent to each follower.

The problem I have is if two user's were to simultaneously run a command that warrant's a notification. The bot gets kicked offline immediately. So what I need is to add an artificial delay before the notification function is run. Time.sleep() , does not work. all it does it freeze the entire program , and hold every command in a queue. (If two user's ran >follow , it would sleep for 2 seconds , and just run both their commands after the delay)

I'm trying to use the threading module in order to replace time.sleep(). My notification function is the following.

#message is the message to be sent, var is the username to use
def notify(message,var):
      #SQL connect 
      dbconfig = read_db_config()
      conn = MySQLConnection(**dbconfig)
      cursor = conn.cursor()
      #choose all of user's followers
      cursor.execute('select username from users where notifications=0 and username IN (select follower from followers where followed like "{}")'.format(var))
      results = cursor.fetchall()
      #for each , send a PM
      for result in results:
        self.pm.message(ch.User(str(result[0])), message)
      conn.close()  

So how would I use threading to do this? I've tried a couple ways , but let's just go with the worst one.

def example(_):
    username = 'bob'
    # _ is equal to args
    a = notify("{} is now following {}.".format(username,_),username)
    c =threading.Timer(2,a)
    c.start()

This will throw a Nonetype error in response.

Exception in thread Thread-1: Traceback (most recent call last):
File "/usr/lib/python2.7/threading.py", line 810, in __bootstrap_inner self.run() File "/usr/lib/python2.7/threading.py", line 1082, in run self.function(*self.args, **self.kwargs) TypeError: 'NoneType' object is not callable

Note: I think this method will work , there will be a lot of users using the bot at once, so until it breaks this seems like a fix.

Pacified
  • 175
  • 9
  • 1
    Anything in [What is the best way to repeatedly execute a function every x seconds in Python?](https://stackoverflow.com/q/474528/2823755) that might help: or [Run certain code every n seconds (duplicate)](https://stackoverflow.com/q/3393612/2823755); or [How to execute a function asynchronously every 60 seconds in Python?](https://stackoverflow.com/q/2223157/2823755) ... – wwii Oct 30 '17 at 21:02
  • hmmm... these are using time.sleep which really doesn't seem to work for me.. also remember it's a bot , so when it's in time.sleep(), all it does is freeze the bot and not allow it to be used. And the requests just pile up. I need any other requests to have to wait until the time is over , before their delay starts. – Pacified Oct 30 '17 at 21:06
  • Instead of having `notify()` write the message directly by calling `self.pm.message()`, have it write the message to a queue. Have another thread that is reads from that queue, waits some delay, then actually sends the message. – gammazero Oct 30 '17 at 21:13
  • in other words , in the for loop , for each user found , put each result in a queue , and create a delay for each? would i still use threading for that or another module ? – Pacified Oct 30 '17 at 21:24
  • What are the errors you're getting? It looks like maybe you need a mutex possibly? – MrJLP Oct 30 '17 at 21:36
  • Exception in thread Thread-1: Traceback (most recent call last): File "/usr/lib/python2.7/threading.py", line 810, in __bootstrap_inner self.run() File "/usr/lib/python2.7/threading.py", line 1082, in run self.function(*self.args, **self.kwargs) TypeError: 'NoneType' object is not callable – Pacified Oct 30 '17 at 21:38
  • Notice that you arent passing a callable to [Timer](https://docs.python.org/2/library/threading.html#timer-objects) but the result of the function which is `None` in this case, in order for this to work you could do `from functools import partial` `a = partial(notify, "{} is now following {}}.".format(username,_),username)` so you are actually passing it a callable which will get called 2 second later. – Jose A. García Nov 08 '17 at 23:17
  • It might be worth a look at [PEP 492](https://www.python.org/dev/peps/pep-0492/#new-coroutine-declaration-syntax) if you're using python 3.6 – mikeLundquist Nov 09 '17 at 03:44

2 Answers2

3

Here is some code that may help. Notice the change to how notify handles the results.

import threading
import Queue

def notifier(nq):
    # Read from queue until None is put on queue.
    while True:
        t = nq.get()
        try:
            if t is None:
                break
            func, args = t
            func(*args)
            time.sleep(2) # wait 2 seconds before sending another notice
        finally:
            nq.task_done()


# message is the message to be sent, var is the username to use, nq is the
# queue to put notification on.
def notify(message, var, nq):
    #SQL connect
    dbconfig = read_db_config()
    conn = MySQLConnection(**dbconfig)
    cursor = conn.cursor()
    #choose all of user's followers
    cursor.execute('select username from users where notifications=0 and username IN (select follower from followers where followed like "{}")'.format(var))
    results = cursor.fetchall()
    #for each , send a PM
    for result in results:
        # Put the function to call and its args on the queue.
        args = (ch.User(str(result[0])), message)
        nq.put((self.pm.message, args))
    conn.close()


if __name__ == '__main__':
    # Start a thread to read from the queue.
    nq = Queue.Queue()
    th = threading.Thread(target=notifier, args=(nq,))
    th.daemon = True
    th.start()
    # Run bot code
    # ...
    #
    nq.put(None)
    nq.join() # block until all tasks are done
gammazero
  • 773
  • 6
  • 13
  • The gap you put under th.start() is supposed to be the rest of the bot code? – Pacified Oct 30 '17 at 22:00
  • It would be where you would call/start whatever body of code is calling `notify()`. This could all be put into another startup/initialization function, but is here in the main section as an example. The point is to start up the handler thread first, before running the rest of the bot code. – gammazero Oct 30 '17 at 22:14
  • ok , it seems i will still need a delay , because it reads through the Queue and still kicks the bot after many requests – Pacified Oct 30 '17 at 22:54
  • @Pacified - Fixed answer to include delay – gammazero Oct 30 '17 at 23:36
  • so that added a delay between each message and the delay added up , i got no response from the bot for like 30 seconds. and then it sent all the requests at once and got kicked. – Pacified Oct 31 '17 at 00:33
  • Seems like there is more going on, like something is batching up the messages and then sending them all at once (even if there is a delay between calling `self.pm.message()`). Is there some way to flush pending messages? – gammazero Oct 31 '17 at 00:42
1

I would try using a threading lock like the class I wrote below.

This will cause only one thread to be able to send PM's at any given time.

class NotifyUsers():
    def __init__(self, *args, **kwargs):
        self.notify_lock = threading.Lock()
        self.dbconfig = read_db_config()
        self.conn = MySQLConnection(**dbconfig)
        self.cursor = self.conn.cursor()

    def notify_lock_wrapper(self, message, var):
        self.notify_lock.acquire()
        try:
            self._notify(message, var)
        except:
            # Error handling here
            pass
        finally:
            self.notify_lock.release()

    def _notify(self, message, var):
        #choose all of user's followers
        self.cursor.execute('select username from users where notifications=0 and username IN (select follower from followers where followed like "{}")'.format(var))
        results = self.cursor.fetchall()

        #for each, send a PM
        for result in results:
            self.pm.message(ch.User(str(result[0])), message)
Myles Hollowed
  • 546
  • 4
  • 16