-
Notifications
You must be signed in to change notification settings - Fork 308
Conversation
I think this is a change to |
|
/me takes a deep breath ... |
39569db
to
42871dd
Compare
de1a0a3
to
01c837f
Compare
a809a77
to
566c176
Compare
Started a todo. 😊
|
Yesssss! 05ff710 is the heart of claiming. Now to dig out! |
1f519df
to
9cd01ae
Compare
Taking out of review until I can rebase now that #4335 is in ... |
757b498
to
404107c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good overall - just a few comments.
gratipay/models/package/__init__.py
Outdated
return self.load_team(self.db) | ||
|
||
|
||
def load_team(self, cursor): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be _load_team, or do you intend for this method to be part of the public API for this class?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to _load_team
in 404107c.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you mean 79fa918.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😊
gratipay/models/package/__init__.py
Outdated
linked to this package if there is one, otherwise ``None``. Makes a | ||
database call. | ||
""" | ||
return self.load_team(self.db) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess self.db
is a cursor. It's interesting to see in the class below that it isn't a cursor (or maybe it is, but you do call get_cursor
on it).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self.db
exposes the same run
, one
, all
API as a cursor does, but with less control over the transaction. Docs.
if cursor: | ||
self._set_primary_email(email, cursor) | ||
else: | ||
with self.db.get_cursor() as cursor: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the place where I'm talking about the get_cursor
call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this is a bit of a goofy construction. liberapay/postgres.py#40 would help.
gratipay/models/participant/email.py
Outdated
except IntegrityError: | ||
return VERIFICATION_STYMIED | ||
with self.db.get_cursor() as cursor: | ||
record = self.get_email(email, cursor) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hooray for replacing r
with record
!
gratipay/models/package/__init__.py
Outdated
return cursor.one('SELECT t.*::teams FROM teams t WHERE t.id=%s', (self.team_id,)) | ||
|
||
|
||
def get_or_create_linked_team(self, cursor, owner): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can just make this create_team
or ensure_team
and return None
. You only use the return value in your tests where you can do a separate load_team
to do your assertions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in 404107c.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you mean 79fa918.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😊
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Picks up at #4404 (comment).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Erm, #4404 (comment) is where it becomes useful to have a team object returned (when we implement multi-package claiming). You okay to keep that here, then?
gratipay/models/package/__init__.py
Outdated
team = self.load_team(cursor) | ||
if not team: | ||
slug = str(uuid.uuid4()).lower() | ||
team = Team.insert( slug=slug |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there an equivalent of INSERT OR IGNORE INTO teams ...
that you can use here? Then you can skip the load_team
call above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TIL about INSERT OR IGNORE
. Looks like that's a MySQL thing?
The alternative with Postgres is to try/except, but that doesn't work with cursors. I tried! :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The modern way in postgres ≥ 9.5 is INSERT ... ON CONFLICT DO NOTHING
(https://www.postgresql.org/docs/9.5/static/sql-insert.html). It's possible to do the same thing with older versions of postgresql even inside a cursor by using pgSQL instead of plain SQL (here's an example of UPSERT with pgSQL).
INSERT OR IGNORE
is SQLite (https://sqlite.org/lang_insert.html).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dowski Do you think it's worth the gymnastics here, or are you okay with the extra db call? If concurrency ends up being an issue that could change the equation ...
def ensure_team(self, cursor, owner): | ||
"""Given a db cursor and :py:class:`Participant`, insert into ``teams`` if need be. | ||
""" | ||
team = self._load_team(cursor) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there an implicit transaction here? What if this method is called concurrently? Seems like you could wind up with multiple entries with different UUIDs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
" ... called concurrently with the same input."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If cursors
is actually self.db
then yes, there is an implicit transaction under here. However, the purpose of exposing the cursor
parameter is precisely to allow passing in a proper cursor, so that an entire call chain can operate within one transaction to preserve integrity. In some methods cursor
is optional (e.g.), but cursor
is required and front-loaded here because integrity is an issue here.
The transaction of which ensure_teams
is a part begins in finish_email_verification
. If finish_email_verification
is called concurrently with the same input, then ... I don't know. I've been working most of the day on trying to sort this out. What I have so far is in 8da4f9c.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's the output I'm seeing:
pid-10149 thread-4563636224 (Thread-2) 1492636220.45 loading team
pid-10149 thread-4563636224 (Thread-2) 1492636220.46 old team: n/a
pid-10149 thread-4563636224 (Thread-2) 1492636220.46 slug: 0d8f713b-fc8b-43f8-ad78-638a8d0808c3
pid-10149 thread-4563636224 (Thread-2) 1492636220.47 new team: 0d8f713b-fc8b-43f8-ad78-638a8d0808c3
pid-10149 thread-4563636224 (Thread-2) 1492636220.47 updated, sleeping ...
pid-10149 thread-4563636224 (Thread-2) 1492636222.47 slept
pid-10149 thread-4567842816 (Thread-3) 1492636222.48 loading team
pid-10149 thread-4567842816 (Thread-3) 1492636222.49 old team: n/a
pid-10149 thread-4567842816 (Thread-3) 1492636222.49 slug: ef264dc3-7d62-44d4-a907-ab18c86afa32
pid-10149 thread-4567842816 (Thread-3) 1492636222.49 new team: ef264dc3-7d62-44d4-a907-ab18c86afa32
pid-10149 thread-4567842816 (Thread-3) 1492636222.49 updated, sleeping ...
pid-10149 thread-4567842816 (Thread-3) 1492636224.49 slept
F
================================================== FAILURES ==================================================
______________________ PackageLinking.test_finishing_email_verification_is_thread_safe _______________________
Traceback (most recent call last):
File "/Users/whit537/personal/gratipay/gratipay.com/tests/py/test_email.py", line 809, in test_finishing_email_verification_is_thread_safe
assert results[b.ident] == _email.VERIFICATION_FAILED
AssertionError: assert 5 == 1
+ where 1 = _email.VERIFICATION_FAILED
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That looks to me like there's something hidden from view that is synchronizing the threads. Even though I start them in parallel, they appear to run in sequence. Hmm ... 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah! I think it's because the ... well, wait ...
I was gonna say because the calls synchronize on the cursor.run('DELETE ...')
call (why that one instead of the SELECT
s? Is psycopg2/Postgres being fancy?), so that the second cursor sees a certain state following the lock release, ... but wouldn't a second insert on teams_to_packages
still fail?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here we go: team is not None
the second time around.
tests/py/test_email.py pid-36042 thread-4463992832 (Thread-2) have cursor, will travel
pid-36042 thread-4463992832 (Thread-2) have record:Record(id=118, address=u'[email protected]', verified=
None, nonce=u'fccf6dec-7559-4cb2-ba77-046f2aa0efe7', verification_start=datetime.datetime(2017, 4, 24, 13, 44, 6, 588594, tzinfo=psycopg2.tz.FixedOffsetTimezone(offset=0, name=None)), verification_end=None, participant_id=1L)
pid-36042 thread-4463992832 (Thread-2) have packages:[<gratipay.models.package.Package object at 0x109d09250>]
pid-36042 thread-4463992832 (Thread-2) let's finish up ...
pid-36042 thread-4468199424 (Thread-3) have cursor, will travel
pid-36042 thread-4463992832 (Thread-2) finishing up ...
pid-36042 thread-4463992832 (Thread-2) team: None
pid-36042 thread-4468199424 (Thread-3) have record:Record(id=118, address=u'[email protected]', verified=None, nonce=u'fccf6dec-7559-4cb2-ba77-046f2aa0efe7', verification_start=datetime.datetime(2017, 4, 24, 13, 44, 6, 588594, tzinfo=psycopg2.tz.FixedOffsetTimezone(offset=0, name=None)), verification_end=None, participant_id=1L)
pid-36042 thread-4468199424 (Thread-3) have packages:[<gratipay.models.package.Package object at 0x109d12250>]
pid-36042 thread-4463992832 (Thread-2) inserting into teams_to_packages: 45 95
pid-36042 thread-4468199424 (Thread-3) let's finish up ...
pid-36042 thread-4463992832 (Thread-2) inserted into teams
pid-36042 thread-4463992832 (Thread-2) [Record(team_id=45L, package_id=95L)]
pid-36042 thread-4468199424 (Thread-3) finishing up ...
pid-36042 thread-4468199424 (Thread-3) team: 80e34bf6-324a-4b63-a700-c3fe3b624001
diff --git a/gratipay/models/package/__init__.py b/gratipay/models/package/__init__.py
index a0591a1..9a69037 100644
--- a/gratipay/models/package/__init__.py
+++ b/gratipay/models/package/__init__.py
@@ -65,7 +65,10 @@ class Package(Model):
def ensure_team(self, cursor, owner):
"""Given a db cursor and :py:class:`Participant`, insert into ``teams`` if need be.
"""
+ from aspen import log
+ log('finishing up ...')
team = self._load_team(cursor)
+ log('team: {}'.format(team.name if team else 'None'))
if not team:
slug = str(uuid.uuid4()).lower()
team = Team.insert( slug=slug
@@ -76,6 +79,7 @@ class Package(Model):
, owner=owner
, _cursor=cursor
)
+ log('inserting into teams_to_packages: {} {}'.format(team.id, self.id))
cursor.run('INSERT INTO teams_to_packages (team_id, package_id) '
'VALUES (%s, %s)', (team.id, self.id))
from aspen import log
diff --git a/gratipay/models/participant/email.py b/gratipay/models/participant/email.py
index e47cafe..5ae2528 100644
--- a/gratipay/models/participant/email.py
+++ b/gratipay/models/participant/email.py
@@ -228,13 +228,17 @@ class Email(object):
def finish_email_verification(self, email, nonce):
+ from aspen import log
if '' in (email.strip(), nonce.strip()):
return VERIFICATION_MISSING
with self.db.get_cursor() as cursor:
+ log('have cursor, will travel')
record = self.get_email(email, cursor)
+ log('have record:' + str(record))
if record is None:
return VERIFICATION_FAILED
packages = self.get_packages_claiming(cursor, nonce)
+ log('have packages:' + str(packages))
if record.verified and not packages:
assert record.nonce is None # and therefore, order of conditions matters
return VERIFICATION_REDUNDANT
@@ -243,6 +247,7 @@ class Email(object):
if (utcnow() - record.verification_start) > EMAIL_HASH_TIMEOUT:
return VERIFICATION_EXPIRED
try:
+ log('let\'s finish up ...')
self.finish_package_claims(cursor, nonce, *packages)
self.save_email_address(cursor, email)
cursor.connection.commit()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gosh. Is there a different possible behavior for each cursor call? 😳
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Woo-hoo! 💃 ✊
tests/py/test_email.py pid-40671 thread-4572102656 (Thread-2) have cursor, will travel
pid-40671 thread-4572102656 (Thread-2) have record:Record(id=121, address=u'[email protected]', verified=None, nonce=u'3193076d-96ce-4864-b927-d5dedb9ff75c', verification_start=datetime.datetime(2017, 4, 24, 14, 6, 58, 165862, tzinfo=psycopg2.tz.FixedOffsetTimezone(offset=0, name=None)), verification_end=None, participant_id=1L)
pid-40671 thread-4572102656 (Thread-2) have packages:[<gratipay.models.package.Package object at 0x110421210>]
pid-40671 thread-4572102656 (Thread-2) let's finish up ...
pid-40671 thread-4576309248 (Thread-3) have cursor, will travel
pid-40671 thread-4572102656 (Thread-2) finishing up ...
pid-40671 thread-4572102656 (Thread-2) team: None
pid-40671 thread-4572102656 (Thread-2) inserting into teams_to_packages: 47 98
pid-40671 thread-4572102656 (Thread-2) inserted into teams
pid-40671 thread-4572102656 (Thread-2) [Record(team_id=47L, package_id=98L)]
pid-40671 thread-4576309248 (Thread-3) have record:Record(id=121, address=u'[email protected]', verified=True, nonce=None, verification_start=datetime.datetime(2017, 4, 24, 14, 6, 58, 165862, tzinfo=psycopg2.tz.FixedOffsetTimezone(offset=0, name=None)), verification_end=datetime.datetime(2017, 4, 24, 14, 6, 58, 192552, tzinfo=psycopg2.tz.FixedOffsetTimezone(offset=0, name=None)), participant_id=1L)
pid-40671 thread-4576309248 (Thread-3) have packages:[]
.
================================ 86 tests deselected by '-kthread_safe' =================================
================================ 1 passed, 86 deselected in 3.54 seconds ================================
diff --git a/gratipay/models/package/__init__.py b/gratipay/models/package/__init__.py
index a0591a1..9a69037 100644
--- a/gratipay/models/package/__init__.py
+++ b/gratipay/models/package/__init__.py
@@ -65,7 +65,10 @@ class Package(Model):
def ensure_team(self, cursor, owner):
"""Given a db cursor and :py:class:`Participant`, insert into ``teams`` if need be.
"""
+ from aspen import log
+ log('finishing up ...')
team = self._load_team(cursor)
+ log('team: {}'.format(team.name if team else 'None'))
if not team:
slug = str(uuid.uuid4()).lower()
team = Team.insert( slug=slug
@@ -76,6 +79,7 @@ class Package(Model):
, owner=owner
, _cursor=cursor
)
+ log('inserting into teams_to_packages: {} {}'.format(team.id, self.id))
cursor.run('INSERT INTO teams_to_packages (team_id, package_id) '
'VALUES (%s, %s)', (team.id, self.id))
from aspen import log
diff --git a/gratipay/models/participant/email.py b/gratipay/models/participant/email.py
index e47cafe..bd6260d 100644
--- a/gratipay/models/participant/email.py
+++ b/gratipay/models/participant/email.py
@@ -228,13 +228,17 @@ class Email(object):
def finish_email_verification(self, email, nonce):
+ from aspen import log
if '' in (email.strip(), nonce.strip()):
return VERIFICATION_MISSING
with self.db.get_cursor() as cursor:
- record = self.get_email(email, cursor)
+ log('have cursor, will travel')
+ record = self.get_email(email, cursor, and_lock=True)
+ log('have record:' + str(record))
if record is None:
return VERIFICATION_FAILED
packages = self.get_packages_claiming(cursor, nonce)
+ log('have packages:' + str(packages))
if record.verified and not packages:
assert record.nonce is None # and therefore, order of conditions matters
return VERIFICATION_REDUNDANT
@@ -243,6 +247,7 @@ class Email(object):
if (utcnow() - record.verification_start) > EMAIL_HASH_TIMEOUT:
return VERIFICATION_EXPIRED
try:
+ log('let\'s finish up ...')
self.finish_package_claims(cursor, nonce, *packages)
self.save_email_address(cursor, email)
cursor.connection.commit()
@@ -295,15 +300,19 @@ class Email(object):
)
- def get_email(self, address, cursor=None):
+ def get_email(self, address, cursor=None, and_lock=False):
"""Return a record for a single email address on file for this participant.
+
+ :param unicode address: the email address for which to get a record
+ :param Cursor cursor: a database cursor; if ``None``, we'll use ``self.db``
+ :param and_lock: if True, we will acquire a write-lock for you on the row
+ :returns: a database record (named tuple)
+
"""
- return (cursor or self.db).one("""
- SELECT *
- FROM emails
- WHERE participant_id=%s
- AND address=%s
- """, (self.id, address))
+ sql = 'SELECT * FROM emails WHERE participant_id=%s AND address=%s'
+ if and_lock:
+ sql += ' FOR UPDATE'
+ return (cursor or self.db).one(sql, (self.id, address))
def get_emails(self):
Iiiinnntteerreeeesssssstttiiinnnggg ...
diff --git a/gratipay/models/package/__init__.py b/gratipay/models/package/__init__.py
index 8ec1ac5..e6963df 100644
--- a/gratipay/models/package/__init__.py
+++ b/gratipay/models/package/__init__.py
@@ -65,6 +65,15 @@ class Package(Model):
def ensure_team(self, cursor, owner):
"""Given a db cursor and :py:class:`Participant`, insert into ``teams`` if need be.
"""
+
+ #########################################
+ from aspen import log
+ import time
+ for i in range(10):
+ log(owner.username)
+ time.sleep(0.1)
+ #########################################
+
import time
from aspen import log as _log
|
Synchronized: diff --git a/gratipay/models/participant/email.py b/gratipay/models/participant/email.py
index 4275821..20b9cd6 100644
--- a/gratipay/models/participant/email.py
+++ b/gratipay/models/participant/email.py
@@ -283,6 +283,15 @@ class Email(object):
cursor.run('DELETE FROM claims WHERE nonce=%s', (nonce,))
package_ids = []
for package in packages:
+
+ #########################################
+ from aspen import log
+ import time
+ for i in range(10):
+ log()
+ time.sleep(0.1)
+ #########################################
+
package.ensure_team(cursor, self)
package_ids.append(package.id)
self.app.add_event( cursor |
Parallel! diff --git a/gratipay/models/participant/email.py b/gratipay/models/participant/email.py
index 4275821..ba9909d 100644
--- a/gratipay/models/participant/email.py
+++ b/gratipay/models/participant/email.py
@@ -243,6 +243,15 @@ class Email(object):
if (utcnow() - record.verification_start) > EMAIL_HASH_TIMEOUT:
return VERIFICATION_EXPIRED
try:
+
+ #########################################
+ from aspen import log
+ import time
+ for i in range(10):
+ log(nonce)
+ time.sleep(0.1)
+ #########################################
+
self.finish_package_claims(cursor, nonce, *packages)
self.save_email_address(cursor, email)
except IntegrityError: |
Eeeeee! Parallel! diff --git a/gratipay/models/participant/email.py b/gratipay/models/participant/email.py
index 4275821..a8fa11c 100644
--- a/gratipay/models/participant/email.py
+++ b/gratipay/models/participant/email.py
@@ -280,6 +280,15 @@ class Email(object):
def finish_package_claims(self, cursor, nonce, *packages):
"""Create teams if needed and associate them with the packages.
"""
+
+ #########################################
+ from aspen import log
+ import time
+ for i in range(10):
+ log(nonce)
+ time.sleep(0.1)
+ #########################################
+
cursor.run('DELETE FROM claims WHERE nonce=%s', (nonce,))
package_ids = []
for package in packages: |
Parallel, and then synchronized. Wow. A curveball! What is going on? 😳
diff --git a/gratipay/models/participant/email.py b/gratipay/models/participant/email.py
index 4275821..b72daa8 100644
--- a/gratipay/models/participant/email.py
+++ b/gratipay/models/participant/email.py
@@ -280,9 +280,19 @@ class Email(object):
def finish_package_claims(self, cursor, nonce, *packages):
"""Create teams if needed and associate them with the packages.
"""
+
+ def spew():
+ import aspen, time
+ for i in range(10):
+ aspen.log(nonce)
+ time.sleep(0.1)
+
+ spew()
+
cursor.run('DELETE FROM claims WHERE nonce=%s', (nonce,))
package_ids = []
for package in packages:
+ spew()
package.ensure_team(cursor, self)
package_ids.append(package.id)
self.app.add_event( cursor |
Okay! diff --git a/gratipay/models/participant/email.py b/gratipay/models/participant/email.py
index 4275821..60dd0fe 100644
--- a/gratipay/models/participant/email.py
+++ b/gratipay/models/participant/email.py
@@ -280,7 +280,15 @@ class Email(object):
def finish_package_claims(self, cursor, nonce, *packages):
"""Create teams if needed and associate them with the packages.
"""
+
+ def spew(msg):
+ import aspen, time
+ for i in range(10):
+ aspen.log(msg)
+ time.sleep(0.1)
+ spew('parallel')
cursor.run('DELETE FROM claims WHERE nonce=%s', (nonce,))
+ spew('serial')
package_ids = []
for package in packages:
package.ensure_team(cursor, self) |
Sorry, I meant for all of those to go under #4397 (comment). Moving back there ... |
I'm good with merging this generally, but what about those failures below? |
8da4f9c
to
f9a9fcc
Compare
Those are from the commit where I was hacking and slashing to sort out concurrency. Pushed off now (was 8da4f9c). What about the fact that calling |
Ah right - understanding that does seem like a good idea. |
Hung up on #4413 ... |
Back from #4413. Now how about this concurrency issue ... |
3d8fb44
to
28dffdd
Compare
Ready again, @dowski. |
baf8248
to
521d355
Compare
9f9c87e
to
1d42094
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
Part of #4305, follow-on from #4335.
Requirements
(9) from the workflow … the big one!
Todo
{email,nonce} == ' '