-
Notifications
You must be signed in to change notification settings - Fork 184
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
for MPP-2223: add RelayNumber.enabled field #2259
Conversation
✅ Deploy Preview for fx-relay-demo canceled.
|
def test_relaynumber_suggestions_bad_request_for_user_without_real_phone(phone_user): | ||
real_phone = "+12223334444" | ||
RealPhone.objects.create(user=phone_user, verified=True, number=real_phone) | ||
def test_relaynumber_post_with_existing_returns_error(phone_user, mocked_twilio_client): | ||
number = "+12223334444" | ||
relay_number = "+19998887777" | ||
RelayNumber.objects.create(user=phone_user, number=relay_number) | ||
RealPhone.objects.create(user=phone_user, number=number, verified=True) |
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 change looks weird. Basically, I saw that these 2 tests (test_relaynumber_suggestions_bad_request_for_user_without_real_phone
and test_relaynumber_post_with_existing_returns_error
) had the wrong names. So I switched the names so the test names would match what the test is actually doing. But this is how git interpreted that change. 🤷
@@ -155,13 +155,16 @@ class RelayNumber(models.Model): | |||
vcard_lookup_key = models.CharField( | |||
max_length=6, default=vcard_lookup_key_default, unique=True | |||
) | |||
enabled = models.BooleanField(default=True) |
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.
All new fields either need nullable=True
, or we need to set a server default in the migration. Otherwise, we'll have problems with the deploy, when the existing servers that don't know about the new field try to write to the database migrated by the new code.
The nullable=True
has been a real pain, let's try to figure out the server default...
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.
Hrm ... when I ran the migration on my local it didn't complain. Maybe I didn't have any existing records when I ran it? Or does django know enough that if default=True
is in the field definition, it sets all existing records to True
?
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.
Update: Added the PostgreSQL version, fixed the sqlmigrate command*
The problem is not running the migration, the problem is when we deploy it. The Kubernetes rollout preserves the number of web pods by replacing old ones with new ones, one at a time, so the old pods will run against the new database schema. You can replicate this with:
- Check out this PR
- Run migration 0017
- Check out tag
v3.6.1
(ormain
, close enough) - Go through the steps to create a Relay number (register a real phone, setup a relay number)
Steps 4 is hard, but you'll end up where the old code is running:
INSERT INTO "phones_relaynumber"
("id", "number", "location", "user_id", "vcard_lookup_key")
VALUES (123, '+1234567890', 'location', 456, 'acb123');
resulting in the error:
django.db.utils.IntegrityError: phones_relaynumber.enabled NOT NULL constraint failed
It was a little easier to see with PR #2232, where a integer column was added, and you could use the UI to fail to create a new alias.
So, we'll get database errors and data loss during the rollout. There's two ways to deal with this. One is to add null=True
, the other is a database default.
The deploy sequence with null=True
:
- Deploy One:
- A migration adds a column with
NULL
allowed. - Old code omits the new column on inserts and updates, and new rows get a
null
value. - The new code needs to expect a
null
value and interpret it as the default (in this caseTrue
).
- A migration adds a column with
- Deploy Two:
- A data migration replaces all
NULL
values withTrue
, and a schema migration changesNULL
toNOT NULL
. - The old code continues to set to
true
orfalse
, and checks fornull
even though is won't happen. - The new code can drop the
null
checks and assume onlytrue
orfalse
.
- A data migration replaces all
The deploy sequence with a database default:
- Deploy One:
- A migration adds a column with a database default
- Old code omits the new column on inserts and updates, and new rows get the database default
- The new code gets either
true
orfalse
, nevernull
- Deploy Two:
- A migration drops the database default
- The old code and the new code are identical, always setting the column to
true
orfalse
The database default is a lot cleaner - the code doesn't change, and no mucking about with null
values. The problem is that Django doesn't support database defaults (issue #470), so you can't do it without writing your own migrations. And that is hard because we have to support both SQLite3 and PostgreSQL.
You can see what Django does with ./manage.py sqlmigrate phones 0017
:
BEGIN;
--
-- Add field enabled to relaynumber
--
CREATE TABLE "new__phones_relaynumber" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "enabled" bool NOT NULL, "number" varchar(15) NOT NULL, "location" varchar(255) NOT NULL, "user_id" integer NOT NULL REFERENCES "auth_user" ("id") DEFERRABLE INITIALLY DEFERRED, "vcard_lookup_key" varchar(6) NOT NULL UNIQUE);
INSERT INTO "new__phones_relaynumber" ("id", "number", "location", "user_id", "vcard_lookup_key", "enabled") SELECT "id", "number", "location", "user_id", "vcard_lookup_key", 1 FROM "phones_relaynumber";
DROP TABLE "phones_relaynumber";
ALTER TABLE "new__phones_relaynumber" RENAME TO "phones_relaynumber";
CREATE INDEX "phones_relaynumber_number_742e5d6b" ON "phones_relaynumber" ("number");
CREATE INDEX "phones_relaynumber_user_id_62c65ede" ON "phones_relaynumber" ("user_id");
COMMIT;
The sequence is:
- Create a new table
new__phones_relaynumber
with theenable
column - Copy all the data from
phones_relaynumber
, setting theenable
column to1
(true) - Drop
phones_relaynumber
- Rename
new__phones_relaynumber
tophones_relaynumber
- Re-add the indexes
This is what the Django docs say for sqlite3 migrations:
SQLite has very little built-in schema alteration support, and so Django attempts to emulate it by:
- Creating a new table with the new schema
- Copying the data across
- Dropping the old table
- Renaming the new table to match the original name
The PostgreSQL looks like:
BEGIN;
--
-- Add field enabled to relaynumber
--
ALTER TABLE "phones_relaynumber" ADD COLUMN "enabled" boolean DEFAULT true NOT NULL;
ALTER TABLE "phones_relaynumber" ALTER COLUMN "enabled" DROP DEFAULT;
COMMIT;
There's a note for PostgreSQL:
The only caveat is that prior to PostgreSQL 11, adding columns with default values causes a full rewrite of the table, for a time proportional to its size. For this reason, it’s recommended you always create new columns with null=True, as this way they will be added immediately.
We're running PostgreSQL 11.15.
With a database-enabled default value, the existing v3.6.1 code will continue to work during the period when this migration is run and the new release is not fully deployed.
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.
Thanks @groovecoder, I'm glad we're adding enabling / disabling this early in the feature!
Please run black
on the codebase, and consider merging PR #2261 into this PR.
…ult-mpp-2223 Add a database default for relaynumber.enabled
16dc7cc
to
7cd0dfd
Compare
@jwhitlock this is ready for a final(?) review. |
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.
Thanks @groovecoder! I checked it out, it looks good.
I think that instead of calling client.messages.create
in api.views.phones.inbound_sms
, we could respond with a Message verb:
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Message to="+user_real_num" from="+mask_num">
[Relay 📲 +incoming_num] Message
</Message>
</Response>
but that's a different experiment.
I filed https://mozilla-hub.atlassian.net/browse/MPP-2232 for us to look into returning TwiML to forward messages. |
New feature description
Adds a
PATCH /api/v1/relaynumber/{id}
endpoint to enable/disable the newRelayNumber.enabled
field, which controls forwarding/blocking all calls and messages.Screenshot (if applicable)
How to test
enabled: true
.enabled: false
.Checklist