Skip to content

Commit 4c97b45

Browse files
authored
Merge pull request CloudBotIRC#165 from linuxdaemon/gonzobot+duck-concurrency-fixes
Fix various race conditions in duckhunt.py
2 parents 8072661 + d23caa3 commit 4c97b45

1 file changed

Lines changed: 55 additions & 24 deletions

File tree

plugins/duckhunt.py

Lines changed: 55 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import operator
22
import random
33
from collections import defaultdict
4-
from time import time
4+
from threading import Lock
5+
from time import time, sleep
56

67
from sqlalchemy import Table, Column, String, Integer, PrimaryKeyConstraint, desc, Boolean
78
from sqlalchemy.sql import select
@@ -65,6 +66,7 @@
6566
MSG_DELAY = 10
6667
MASK_REQ = 3
6768
scripters = defaultdict(int)
69+
chan_locks = defaultdict(lambda: defaultdict(Lock))
6870
game_status = defaultdict(lambda: defaultdict(lambda: defaultdict(int)))
6971

7072

@@ -93,21 +95,46 @@ def load_status(db):
9395
set_ducktime(chan, net)
9496

9597

98+
def save_channel_state(db, network, chan, status=None):
99+
if status is None:
100+
status = game_status[network][chan.casefold()]
101+
102+
active = bool(status['game_on'])
103+
duck_kick = bool(status['no_duck_kick'])
104+
res = db.execute(status_table.update().where(status_table.c.network == network).where(
105+
status_table.c.chan == chan).values(
106+
active=active, duck_kick=duck_kick
107+
))
108+
if not res.rowcount:
109+
db.execute(status_table.insert().values(network=network, chan=chan, active=active, duck_kick=duck_kick))
110+
111+
db.commit()
112+
113+
96114
@hook.on_unload
97-
@hook.periodic(5 * 60, initial_interval=10 * 60)
98-
def save_status(db):
115+
def save_on_exit(db):
116+
return save_status(db, False)
117+
118+
119+
# @hook.periodic(8 * 3600, singlethread=True) # Run every 8 hours
120+
def save_status(db, _sleep=True):
99121
for network in game_status:
100122
for chan, status in game_status[network].items():
101-
active = bool(status['game_on'])
102-
duck_kick = bool(status['no_duck_kick'])
103-
res = db.execute(status_table.update().where(status_table.c.network == network).where(
104-
status_table.c.chan == chan).values(
105-
active=active, duck_kick=duck_kick
106-
))
107-
if not res.rowcount:
108-
db.execute(status_table.insert().values(network=network, chan=chan, active=active, duck_kick=duck_kick))
123+
save_channel_state(db, network, chan, status)
124+
125+
if _sleep:
126+
sleep(5)
127+
109128

110-
db.commit()
129+
def set_game_state(db, conn, chan, active=None, duck_kick=None):
130+
status = game_status[conn.name][chan]
131+
if active is not None:
132+
status['game_on'] = int(active)
133+
134+
if duck_kick is not None:
135+
status['no_duck_kick'] = int(duck_kick)
136+
137+
save_channel_state(db, conn.name, chan, status)
111138

112139

113140
@hook.event([EventType.message, EventType.action], singlethread=True)
@@ -123,7 +150,7 @@ def incrementMsgCounter(event, conn):
123150

124151

125152
@hook.command("starthunt", autohelp=False, permissions=["chanop", "op", "botcontrol"])
126-
def start_hunt(chan, message, conn):
153+
def start_hunt(db, chan, message, conn):
127154
"""- This command starts a duckhunt in your channel, to stop the hunt use .stophunt"""
128155
global game_status
129156
if chan in opt_out:
@@ -134,7 +161,8 @@ def start_hunt(chan, message, conn):
134161
if check:
135162
return "there is already a game running in {}.".format(chan)
136163
else:
137-
game_status[conn.name][chan]['game_on'] = 1
164+
set_game_state(db, conn, chan, active=True)
165+
138166
set_ducktime(chan, conn.name)
139167
message(
140168
"Ducks have been spotted nearby. See how many you can shoot or save. use .bang to shoot or .befriend to save them. NOTE: Ducks now appear as a function of time and channel activity.",
@@ -153,29 +181,29 @@ def set_ducktime(chan, conn):
153181

154182

155183
@hook.command("stophunt", autohelp=False, permissions=["chanop", "op", "botcontrol"])
156-
def stop_hunt(chan, conn):
184+
def stop_hunt(db, chan, conn):
157185
"""- This command stops the duck hunt in your channel. Scores will be preserved"""
158186
global game_status
159187
if chan in opt_out:
160188
return
161189
if game_status[conn.name][chan]['game_on']:
162-
game_status[conn.name][chan]['game_on'] = 0
190+
set_game_state(db, conn, chan, active=False)
163191
return "the game has been stopped."
164192
else:
165193
return "There is no game running in {}.".format(chan)
166194

167195

168196
@hook.command("duckkick", permissions=["chanop", "op", "botcontrol"])
169-
def no_duck_kick(text, chan, conn, notice_doc):
197+
def no_duck_kick(db, text, chan, conn, notice_doc):
170198
"""<enable|disable> - If the bot has OP or half-op in the channel you can specify .duckkick enable|disable so that people are kicked for shooting or befriending a non-existent goose. Default is off."""
171199
global game_status
172200
if chan in opt_out:
173201
return
174202
if text.lower() == 'enable':
175-
game_status[conn.name][chan]['no_duck_kick'] = 1
203+
set_game_state(db, conn, chan, duck_kick=True)
176204
return "users will now be kicked for shooting or befriending non-existent ducks. The bot needs to have appropriate flags to be able to kick users for this to work."
177205
elif text.lower() == 'disable':
178-
game_status[conn.name][chan]['no_duck_kick'] = 0
206+
set_game_state(db, conn, chan, duck_kick=False)
179207
return "kicking for non-existent ducks has been disabled."
180208
else:
181209
notice_doc()
@@ -286,7 +314,7 @@ def update_score(nick, chan, db, conn, shoot=0, friend=0):
286314
return {'shoot': shoot, 'friend': friend}
287315

288316

289-
def attack(nick, chan, message, db, conn, notice, attack):
317+
def attack(event, nick, chan, message, db, conn, notice, attack):
290318
global game_status, scripters
291319
if chan in opt_out:
292320
return
@@ -361,22 +389,25 @@ def attack(nick, chan, message, db, conn, notice, attack):
361389
score = update_score(nick, chan, db, conn, **args)[attack_type]
362390
except Exception:
363391
status['duck_status'] = 1
392+
event.reply("An unknown error has occurred.")
364393
raise
365394

366395
message(msg.format(nick, shoot - deploy, pluralize(score, "duck"), chan))
367396
set_ducktime(chan, conn.name)
368397

369398

370399
@hook.command("bang", autohelp=False)
371-
def bang(nick, chan, message, db, conn, notice):
400+
def bang(nick, chan, message, db, conn, notice, event):
372401
"""- when there is a duck on the loose use this command to shoot it."""
373-
return attack(nick, chan, message, db, conn, notice, "shoot")
402+
with chan_locks[conn.name][chan.casefold()]:
403+
return attack(event, nick, chan, message, db, conn, notice, "shoot")
374404

375405

376406
@hook.command("befriend", autohelp=False)
377-
def befriend(nick, chan, message, db, conn, notice):
407+
def befriend(nick, chan, message, db, conn, notice, event):
378408
"""- when there is a duck on the loose use this command to befriend it before someone else shoots it."""
379-
return attack(nick, chan, message, db, conn, notice, "befriend")
409+
with chan_locks[conn.name][chan.casefold()]:
410+
return attack(event, nick, chan, message, db, conn, notice, "befriend")
380411

381412

382413
def smart_truncate(content, length=320, suffix='...'):

0 commit comments

Comments
 (0)