-
-
Notifications
You must be signed in to change notification settings - Fork 402
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Race Condition With Database Writes (set_nick_value()
)
#2479
Comments
I'm not able to repro this with the latest master and python 3.10.6 |
Ah. After completely blowing away the database, I can repro this. |
He is now. :P |
Might be reasonable for "this is a new nick I haven't seen before" to be handled somewhere in core before plugins ever have a chance to trigger, maybe a (Yes, it'd still be possible for plugins to intentionally attempt to have their code run before |
I can reproduce this on both 3.10 and 3.7. This is a TOCTOU issue in A user plugin should be able to avoid this issue in 7.1.9 or against development (7bcb942) by adding
I'm a -1 on this since it means doing work in the core that may be serving a plugin that isn't even enabled. I imagine most Sopel instances do enable If we declare the |
Think I was too brief before. I don't mean "a nick I haven't seen before" in the context of Maybe it would eliminate this particular race/TOCTOU, or maybe not—but it also seems unnecessarily laissez-faire to wait until someone calls (All that said, the database schema really needs a revision to combine the |
I'm a little more sure now what this solution would look like. Making the Click for patch diff
diff --git a/sopel/db.py b/sopel/db.py
index 2763495f..191d6d1c 100644
--- a/sopel/db.py
+++ b/sopel/db.py
@@ -24,7 +24,7 @@ import typing
from sqlalchemy import Column, create_engine, ForeignKey, Integer, String
from sqlalchemy.engine.url import make_url, URL
-from sqlalchemy.exc import OperationalError
+from sqlalchemy.exc import OperationalError, IntegrityError
from sqlalchemy.orm import declarative_base, scoped_session, sessionmaker
from sqlalchemy.sql import delete, func, select, update
@@ -68,7 +68,7 @@ class Nicknames(BASE):
__tablename__ = 'nicknames'
__table_args__ = MYSQL_TABLE_ARGS
nick_id = Column(Integer, ForeignKey('nick_ids.nick_id'), primary_key=True)
- slug = Column(String(255), primary_key=True)
+ slug = Column(String(255), primary_key=True, unique=True)
canonical = Column(String(255))
@@ -397,14 +397,26 @@ class SopelDB:
session.add(nick_id)
session.commit()
- # Create a new Nickname
- nickname = Nicknames(
- nick_id=nick_id.nick_id,
- slug=slug,
- canonical=nick,
- )
- session.add(nickname)
- session.commit()
+ try:
+ # Create a new Nickname
+ nickname = Nicknames(
+ nick_id=nick_id.nick_id,
+ slug=slug,
+ canonical=nick,
+ )
+ session.add(nickname)
+ session.commit()
+ except IntegrityError as exc:
+ if exc.args and "UNIQUE constraint failed: nicknames.slug" in exc.args[0]:
+ LOGGER.warning("Data race detected when calling get_nick_id()")
+ # another thread beat us to creating this entry, rollback and query again
+ session.rollback()
+ nickname = session.execute(
+ select(Nicknames).where(Nicknames.slug == slug)
+ ).scalar_one_or_none()
+ else:
+ # something else went wrong, re-raise
+ raise However, this patch is incomplete, because a new
It sounds like either the core task or DB revision that @dgw proposes would avoid this problem, with the core task neatly avoiding this issue altogether. I would be in favor of promptly populating the table before any plugins (including official ones) have the option to cause havoc, but I'm not familiar enough with these Sopel guts to know what that looks like. |
I don't want to further slow |
Since And in the future, once we sort out the path toward proper database migrations (#1784), some simplified version of the ridiculous three-table schema we have for nicknames can be applied to channels, so they too can benefit from case-insensitivity. But I'm getting way ahead of this issue, huh? |
Description
If two commands try to write a nick value (
set_nick_value()
) for the a nickname at the same time, before that nickname exists in the database, you will end up with multiplenick_ids
for onenicknames
.Reproduction steps
default.cfg
so that Sopel will connect to your desired server and channel.test.py
(see below) to your/plugins
subfoldernicknames
table that one nick has twonick_id
s associated with it.test.py
:Expected behavior
There should only be one nick_id created for one nickname.
Relevant logs
Notes
Make sure you're testing with with a fresh database or using nicks your bot has never seen before.
Sopel version
v8.0.0.dev0
Installation method
pip install
Python version
3.10.12
Operating system
No response
IRCd
No response
Relevant plugins
seen.py
The text was updated successfully, but these errors were encountered: