-
-
Notifications
You must be signed in to change notification settings - Fork 218
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
Create RepeaterStubs #29599
base: master
Are you sure you want to change the base?
Create RepeaterStubs #29599
Conversation
@@ -402,3 +400,122 @@ def test_yes(self): | |||
with make_repeat_record(self.repeater_stub, RECORD_PENDING_STATE): | |||
is_migrated = are_repeat_records_migrated(DOMAIN) | |||
self.assertTrue(is_migrated) | |||
|
|||
|
|||
class RepeaterStubOneToOneRepeaterTests(RepeaterFixtureMixin, TestCase): |
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.
F821 undefined name 'RepeaterFixtureMixin'
).count(), 0) | ||
|
||
|
||
class TestMigrationCantDuplicate(RepeaterFixtureMixin, TestCase): |
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.
F821 undefined name 'RepeaterFixtureMixin'
).count(), 1) | ||
|
||
|
||
class PauseResumeRetireRepeaterTests(RepeaterFixtureMixin, TestCase): |
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.
F821 undefined name 'RepeaterFixtureMixin'
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 changes look good, although I'm concerned with how the two systems handle downtime. I'd like to see some test changes, and, if possible, RepeaterStub
to be called anything other than Stub
# It's faster to violate uniqueness constraint and ask | ||
# forgiveness than to use `.get()` or `.exists()` first. | ||
pass |
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.
Wouldn't get_or_create be the canonical way to do this?
|
||
def create_repeaterstubs(apps, schema_editor): | ||
for repeater in iter_repeaters(): | ||
with transaction.atomic(): |
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.
Does this need to be a transaction? Unless something will be added later, this is only performing a single create, which is going to be auto-committed anyhow
|
||
def test_only_one_in_domain(self): | ||
with transaction.atomic(): | ||
# (Run in its own transaction so as not to mess with tearDown) |
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.
What error was this giving you? Failing to create a duplicate object should not affect tearDown
class RepeaterStubOneToOneRepeaterTests(RepeaterFixtureMixin, TestCase): | ||
|
||
def test_one_in_domain(self): | ||
self.assertEqual(RepeaterStub.objects.filter( | ||
domain=DOMAIN, | ||
repeater_id=self.repeater.get_id, |
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.
These tests are difficult to follow. They are deceptively short, because all of the work is done in the setup methods. Ideally, tests should follow the AAA pattern (Arrange, Act, Assert). I want to be able to see what is being constructed, what you are doing on those constructed objects, and what ultimately is the expected result we're asserting against. In this case, all of our arrange blocks are hidden behind multiple layers -- they don't exist in the test body itself, they don't exist in the class, so they're rather cryptic to find in RepeaterFixtureMixin
. Does this behavior warrant 4 tests? It seems like we're just testing Django's models.UniqueConstraint
.
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're right. Test mix-ins are awful, and I'm not sure how practical these tests are. I'll rework this.
self.repeater = FormRepeater( | ||
domain=DOMAIN, | ||
url='https://www.example.com/api/', | ||
) |
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.
FormRepeater
is still a couch model, right? Do our tests get stuck if this repeater already exists?
@@ -185,6 +185,17 @@ def _iter_repeat_records_by_repeater(domain, repeater_id, chunk_size, | |||
yield doc['id'] | |||
|
|||
|
|||
def iter_repeaters(): |
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.
Does iter
convey any useful information? Judging by how it's used, get_all_repeaters()
makes its usage in create_repeaterstubs
a bit easier for me to understand. It was not obvious to me that it was fetching all repeaters
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.
Developers have been frustrated in the past when they discover that get_foo()
returns a generator, instead of a list or tuple that can be iterated more than once. So I use iter_foo()
to make callers aware that they aren't getting a list.
Maybe a type hint would check both boxes?
def get_all_repeaters() -> Generator:
...
|
||
def create_repeaterstubs(apps, schema_editor): | ||
for repeater in iter_repeaters(): | ||
with transaction.atomic(): |
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.
Any thought given to doing this process via bulk_create? It should be substantially faster
constraints = [ | ||
models.UniqueConstraint(fields=['repeater_id'], | ||
name='one_to_one_repeater') | ||
] |
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.
why isn't this done using unique?
def delete(self): | ||
if self.repeater_stub.id: | ||
self.repeater_stub.delete() | ||
super().delete() |
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.
Since these are two separate databases, it seems possible that deleting the stub could succeed, while deleting from the SQL database could fail. Is that a situation that needs to be avoided? If so, it seems like it would be slightly safer to wrap this in a transaction so we can rollback the stub's deletion in the event that the couch deletion fails.
# Deleting RepeaterStub will cascade-delete SQLRepeatRecords | ||
# and SQLRepeatRecordAttempts too. (RequestLog will still keep a | ||
# record of all send attempts.) | ||
self.repeater_stub.delete() | ||
# NOTE: Undeleting a Repeater needs to include creating a | ||
# RepeaterStub for 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.
In this, pause, and resume, are we concerned about what happens if couch or SQL are not both simultaneously available? Again, using SQL transactions might be useful to mitigate that here. It might also be useful to have at least one test that illustrates how we handle when one of the systems is not available
Thanks for the review @mjriley I'll be spending some time changing the way this migration is rolled out, and I'll take the opportunity to implement the feedback you've given here and on the "Switch over" PR. I am looking for suggestions for what to rename "RepeaterStub". Its purpose is to link a Repeater's repeat records, and allow them to be managed collectively (which we aren't doing with Couch repeat records). I don't like the current name either, but I couldn't think of a good name. |
Summary
Repeat Records Couch-to-SQL migration PR 4 of 6
I'm opening this as a draft PR to give context to the conversation around this migration. Following on for a conversation with Danny, this and the two PRs that build on it will need some changes, in order to roll out this migration with more caution.
The changes to these PRs that we discussed are:
Currently this series of PRs introduces a change in behavior enabled by the switch to SQL; instead of iterating repeat records we can iterate repeaters, send payloads in the order they were created, and handle offline services more intelligently. But to reduce the risk inherent in behavior changes, we should deploy the behavior change after the switch to SQL, instead of with it.
Safety Assurance
Automated test coverage
PR includes test coverage. If there are aspects that are not covered by tests, please point them out.
QA Plan
Left to the discretion of the SaaS team.
Safety story
This PR will be subject to change. It is not safe to merge.
Rollback instructions