mirror of https://github.com/zulip/zulip.git
models: New PresenceSequence model and UserPresence.last_update_id col.
Migration plan: 1. Add NULLable .last_update_id column to UserPresence with default 0 for new objects. 2. Backfill the value to 0 for old UserPresences, can be done in the background while server is running. 3. Make the column non-NULL. 4. Add new model PresenceSequence and create its rows for old realms.
This commit is contained in:
parent
6750b02437
commit
0dca8f2a38
|
@ -35,6 +35,7 @@ from zerver.models import (
|
||||||
Stream,
|
Stream,
|
||||||
UserProfile,
|
UserProfile,
|
||||||
)
|
)
|
||||||
|
from zerver.models.presence import PresenceSequence
|
||||||
from zerver.models.realms import (
|
from zerver.models.realms import (
|
||||||
CommonPolicyEnum,
|
CommonPolicyEnum,
|
||||||
InviteToRealmPolicyEnum,
|
InviteToRealmPolicyEnum,
|
||||||
|
@ -287,6 +288,8 @@ def do_create_realm(
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
PresenceSequence.objects.create(realm=realm, last_update_id=0)
|
||||||
|
|
||||||
maybe_enqueue_audit_log_upload(realm)
|
maybe_enqueue_audit_log_upload(realm)
|
||||||
|
|
||||||
# Create channels once Realm object has been saved
|
# Create channels once Realm object has been saved
|
||||||
|
|
|
@ -13,6 +13,7 @@ from zerver.models import (
|
||||||
UserProfile,
|
UserProfile,
|
||||||
)
|
)
|
||||||
from zerver.models.clients import get_client
|
from zerver.models.clients import get_client
|
||||||
|
from zerver.models.presence import PresenceSequence
|
||||||
from zerver.models.users import get_system_bot
|
from zerver.models.users import get_system_bot
|
||||||
from zproject.backends import all_default_backend_names
|
from zproject.backends import all_default_backend_names
|
||||||
|
|
||||||
|
@ -48,6 +49,8 @@ def create_internal_realm() -> None:
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
PresenceSequence.objects.create(realm=realm, last_update_id=0)
|
||||||
|
|
||||||
# Create some client objects for common requests. Not required;
|
# Create some client objects for common requests. Not required;
|
||||||
# just ensures these get low IDs in production, and in development
|
# just ensures these get low IDs in production, and in development
|
||||||
# avoids an extra database write for the first HTTP request in
|
# avoids an extra database write for the first HTTP request in
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Generated by Django 5.0.5 on 2024-05-02 02:17
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
atomic = False
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("zerver", "0524_remove_userprofile_onboarding_steps"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
# Create a new column, making it NULLable to avoid locking the table
|
||||||
|
# rewriting rows with a non-NULL default value.
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="userpresence",
|
||||||
|
name="last_update_id",
|
||||||
|
field=models.PositiveBigIntegerField(db_index=True, null=True),
|
||||||
|
),
|
||||||
|
# This is an SQL noop, since Django doesn't add defaults at database level.
|
||||||
|
# The default guarantees new rows will have a value. Old rows can get backfilled
|
||||||
|
# in the next migration.
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="userpresence",
|
||||||
|
name="last_update_id",
|
||||||
|
field=models.PositiveBigIntegerField(db_index=True, default=0, null=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,57 @@
|
||||||
|
from django.contrib.postgres.operations import AddIndexConcurrently
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||||
|
from django.db.migrations.state import StateApps
|
||||||
|
|
||||||
|
|
||||||
|
def backfill_user_presence_last_update_id(
|
||||||
|
apps: StateApps, schema_editor: BaseDatabaseSchemaEditor
|
||||||
|
) -> None:
|
||||||
|
UserPresence = apps.get_model("zerver", "UserPresence")
|
||||||
|
|
||||||
|
max_id = UserPresence.objects.aggregate(models.Max("id"))["id__max"]
|
||||||
|
if max_id is None:
|
||||||
|
# Nothing to do if there are no rows yet.
|
||||||
|
return
|
||||||
|
|
||||||
|
BATCH_SIZE = 10000
|
||||||
|
lower_bound = 0
|
||||||
|
|
||||||
|
# Add a slop factor to make it likely we run past the end in case
|
||||||
|
# of new rows created while we run. The next step will fail to
|
||||||
|
# remove the null possibility if we race, so this is safe.
|
||||||
|
max_id += BATCH_SIZE / 2
|
||||||
|
|
||||||
|
while lower_bound < max_id:
|
||||||
|
UserPresence.objects.filter(
|
||||||
|
id__gt=lower_bound, id__lte=lower_bound + BATCH_SIZE, last_update_id=None
|
||||||
|
).update(last_update_id=0)
|
||||||
|
lower_bound += BATCH_SIZE
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
atomic = False
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("zerver", "0525_userpresence_last_update_id"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(
|
||||||
|
backfill_user_presence_last_update_id,
|
||||||
|
reverse_code=migrations.RunPython.noop,
|
||||||
|
elidable=True,
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="userpresence",
|
||||||
|
name="last_update_id",
|
||||||
|
field=models.PositiveBigIntegerField(db_index=True, default=0),
|
||||||
|
),
|
||||||
|
AddIndexConcurrently(
|
||||||
|
model_name="userpresence",
|
||||||
|
index=models.Index(
|
||||||
|
fields=["realm", "last_update_id"],
|
||||||
|
name="zerver_userpresence_realm_last_update_id_idx",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,76 @@
|
||||||
|
# Generated by Django 5.0.5 on 2024-05-02 22:36
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||||
|
from django.db.migrations.state import StateApps
|
||||||
|
|
||||||
|
|
||||||
|
def create_presence_sequence_for_old_realms(
|
||||||
|
apps: StateApps, schema_editor: BaseDatabaseSchemaEditor
|
||||||
|
) -> None:
|
||||||
|
Realm = apps.get_model("zerver", "Realm")
|
||||||
|
PresenceSequence = apps.get_model("zerver", "PresenceSequence")
|
||||||
|
|
||||||
|
max_id = Realm.objects.aggregate(models.Max("id"))["id__max"]
|
||||||
|
if max_id is None:
|
||||||
|
# Nothing to do if there are no rows yet.
|
||||||
|
return
|
||||||
|
|
||||||
|
BATCH_SIZE = 2000
|
||||||
|
lower_bound = 0
|
||||||
|
|
||||||
|
# Add a slop factor to make it likely we run past the end in case
|
||||||
|
# of new rows created while we run. Races with realm creation are
|
||||||
|
# pretty unlikely, and should throw an exception, so we should
|
||||||
|
# catch them.
|
||||||
|
max_id += BATCH_SIZE / 2
|
||||||
|
|
||||||
|
while lower_bound < max_id:
|
||||||
|
realm_ids = Realm.objects.filter(
|
||||||
|
id__gt=lower_bound,
|
||||||
|
id__lte=lower_bound + BATCH_SIZE,
|
||||||
|
# Filter to realm whose PresenceSequence does not exist, to avoid
|
||||||
|
# running into IntegrityError by trying to create duplicate PresenceSequence.
|
||||||
|
presencesequence=None,
|
||||||
|
).values_list("id", flat=True)
|
||||||
|
|
||||||
|
PresenceSequence.objects.bulk_create(
|
||||||
|
PresenceSequence(realm_id=realm_id, last_update_id=0) for realm_id in realm_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
lower_bound += BATCH_SIZE
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
atomic = False
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("zerver", "0526_user_presence_backfill_last_update_id_to_0"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="PresenceSequence",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("last_update_id", models.PositiveBigIntegerField()),
|
||||||
|
(
|
||||||
|
"realm",
|
||||||
|
models.OneToOneField(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, to="zerver.realm"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
create_presence_sequence_for_old_realms,
|
||||||
|
reverse_code=migrations.RunPython.noop,
|
||||||
|
elidable=True,
|
||||||
|
),
|
||||||
|
]
|
|
@ -30,6 +30,8 @@ class UserPresence(models.Model):
|
||||||
# queries to fetch all presence data for a given realm.
|
# queries to fetch all presence data for a given realm.
|
||||||
realm = models.ForeignKey(Realm, on_delete=CASCADE)
|
realm = models.ForeignKey(Realm, on_delete=CASCADE)
|
||||||
|
|
||||||
|
last_update_id = models.PositiveBigIntegerField(db_index=True, default=0)
|
||||||
|
|
||||||
# The last time the user had a client connected to Zulip,
|
# The last time the user had a client connected to Zulip,
|
||||||
# including idle clients where the user hasn't interacted with the
|
# including idle clients where the user hasn't interacted with the
|
||||||
# system recently (and thus might be AFK).
|
# system recently (and thus might be AFK).
|
||||||
|
@ -58,6 +60,10 @@ class UserPresence(models.Model):
|
||||||
fields=["realm", "last_connected_time"],
|
fields=["realm", "last_connected_time"],
|
||||||
name="zerver_userpresence_realm_id_last_connected_time_98d2fc9f_idx",
|
name="zerver_userpresence_realm_id_last_connected_time_98d2fc9f_idx",
|
||||||
),
|
),
|
||||||
|
models.Index(
|
||||||
|
fields=["realm", "last_update_id"],
|
||||||
|
name="zerver_userpresence_realm_last_update_id_idx",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -70,6 +76,11 @@ class UserPresence(models.Model):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class PresenceSequence(models.Model):
|
||||||
|
realm = models.OneToOneField(Realm, on_delete=CASCADE)
|
||||||
|
last_update_id = models.PositiveBigIntegerField()
|
||||||
|
|
||||||
|
|
||||||
class UserStatus(AbstractEmoji):
|
class UserStatus(AbstractEmoji):
|
||||||
user_profile = models.OneToOneField(UserProfile, on_delete=CASCADE)
|
user_profile = models.OneToOneField(UserProfile, on_delete=CASCADE)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue