Steve V.
Steve V.

Reputation: 453

Least painful way to run a Python delay loop

I've got an event-driven chatbot and I'm trying to implement spam protection. I want to silence a user who is behaving badly for a period of time, without blocking the rest of the application.

Here's what doesn't work:

if user_behaving_badly():
  ban( user )
  time.sleep( penalty_duration )  # Bad! Blocks the entire application!
  unban( user )

Ideally, if user_behaving_badly() is true, I want to start a new thread which does nothing but ban the user, then sleep for a while, unban the user, and then the thread disappears.

According to this I can accomplish my goal using the following:

if user_behaving_badly():
  thread.start_new_thread( banSleepUnban, ( user, penalty ) )

"Simple" is usually an indicator of "good", and this is pretty simple, but everything I've heard about threads has said that they can bite you in unexpected ways. My question is: Is there a better way than this to run a simple delay loop without blocking the rest of the application?

Upvotes: 2

Views: 2099

Answers (5)

Jake Thompson
Jake Thompson

Reputation: 301

Why thread at all?

do_something(user):
  if(good_user(user)):
    # do it
  else
    # don't

good_user():
  if(is_user_baned(user)):
    if(past_time_since_ban(user)):
      user_good_user(user)
  elif(is_user_bad()):
    ban_user()

ban_user(user):
  # add a user/start time to a hash

is_user_banned()
  # check hash
  # could check if expired now too, or do it seperately if you care about it

is_user_bad()
  # check params or set more values in a hash

Upvotes: 3

Dan D.
Dan D.

Reputation: 74645

instead of starting a thread for each ban, put the bans in a priority queue and have a single thread do the sleeping and unbanning

this code keeps two structures a heapq that allows it to quickly find the soonest ban to expire and a dict to make it possible to quickly check if a user is banned by name

import time
import threading
import heapq

class Bans():
    def __init__(self):
        self.lock = threading.Lock()
        self.event = threading.Event()
        self.heap = []
        self.dict = {}
        self.thread = threading.thread(target=self.expiration_thread)
        self.thread.setDaemon(True)
        self.thread.start()

    def ban_user(self, name, duration):
        with self.lock:
            now = time.time()
            expiration = (now+duration) 
            heapq.heappush(self.heap, (expiration, user))
            self.dict[user] = expiration
            self.event.set()

    def is_user_banned(self, user):
        with self.lock:
            now = time.time()
            return self.dict.get(user, None) > now

    def expiration_thread(self):
        while True:
            self.event.wait()
            with self.lock:
                next, user = self.heap[0]
                now = time.time()
                duration = next-now
            if duration > 0:
                time.sleep(duration)
            with self.lock:
                if self.heap[0][0] = next:
                    heapq.heappop(self.heap)
                    del self.dict(user)
                if not self.heap:
                    self.event.clear()

and is used like this:

B = Bans()
B.ban_user("phil", 30.0)
B.is_user_banned("phil")

Upvotes: 5

tom10
tom10

Reputation: 69182

Use a threading timer object, like this:

t = threading.Timer(30.0, unban)
t.start() # after 30 seconds, unban will be run

Then only unban is run in the thread.

Upvotes: 3

Anti Earth
Anti Earth

Reputation: 4811

If you're using a GUI,
most GUI modules have a timer function which can abstract all the yuck multithreading stuff, and execute code after a given time, though still allowing the rest of the code to be executed.

For instance, Tkinter has the 'after' function.

Upvotes: 0

Chris
Chris

Reputation: 1

This is language agnostic, but consider a thread to keep track of stuff. The thread keeps a data structure that has something like "username" and "banned_until" in a table. The thread is always running in the background checking the table, if banned_until is expired, it unblocks the user. Other threads go on normally.

Upvotes: 0

Related Questions