zulip/analytics/models.py
Alex Vandiver 4f4725f810 analytics: Migrate models' id columns to bigint.
This helps prevent wraparound on exceedingly large and old installs,
particularly Zulip Cloud.  These are relatively simple migrations
since they are not referenced by any other tables; however, they are
quite large, and are actively used from Django by running servers,
making this not a migration which is possible to run without stopping
the server.

Use the escape hatch in the previous commit to temporarily pause
analytics writes while the migration happens.  This should make the
migration transparent to users, at the small cost of an artificial dip
in statistics (specifically, to push notification counts, and unread
message counts) while the migration runs.
2024-06-05 11:48:27 -07:00

165 lines
5.3 KiB
Python

# https://github.com/typeddjango/django-stubs/issues/1698
# mypy: disable-error-code="explicit-override"
from datetime import datetime
from django.db import models
from django.db.models import Q, UniqueConstraint
from typing_extensions import override
from zerver.lib.timestamp import floor_to_day
from zerver.models import Realm, Stream, UserProfile
class FillState(models.Model):
property = models.CharField(max_length=40, unique=True)
end_time = models.DateTimeField()
# Valid states are {DONE, STARTED}
DONE = 1
STARTED = 2
state = models.PositiveSmallIntegerField()
@override
def __str__(self) -> str:
return f"{self.property} {self.end_time} {self.state}"
# The earliest/starting end_time in FillState
# We assume there is at least one realm
def installation_epoch() -> datetime:
earliest_realm_creation = Realm.objects.aggregate(models.Min("date_created"))[
"date_created__min"
]
return floor_to_day(earliest_realm_creation)
class BaseCount(models.Model):
# Note: When inheriting from BaseCount, you may want to rearrange
# the order of the columns in the migration to make sure they
# match how you'd like the table to be arranged.
id = models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
)
property = models.CharField(max_length=32)
subgroup = models.CharField(max_length=16, null=True)
end_time = models.DateTimeField()
value = models.BigIntegerField()
class Meta:
abstract = True
class InstallationCount(BaseCount):
class Meta:
# Handles invalid duplicate InstallationCount data
constraints = [
UniqueConstraint(
fields=["property", "subgroup", "end_time"],
condition=Q(subgroup__isnull=False),
name="unique_installation_count",
),
UniqueConstraint(
fields=["property", "end_time"],
condition=Q(subgroup__isnull=True),
name="unique_installation_count_null_subgroup",
),
]
@override
def __str__(self) -> str:
return f"{self.property} {self.subgroup} {self.value}"
class RealmCount(BaseCount):
realm = models.ForeignKey(Realm, on_delete=models.CASCADE)
class Meta:
# Handles invalid duplicate RealmCount data
constraints = [
UniqueConstraint(
fields=["realm", "property", "subgroup", "end_time"],
condition=Q(subgroup__isnull=False),
name="unique_realm_count",
),
UniqueConstraint(
fields=["realm", "property", "end_time"],
condition=Q(subgroup__isnull=True),
name="unique_realm_count_null_subgroup",
),
]
indexes = [
models.Index(
fields=["property", "end_time"],
name="analytics_realmcount_property_end_time_3b60396b_idx",
)
]
@override
def __str__(self) -> str:
return f"{self.realm!r} {self.property} {self.subgroup} {self.value}"
class UserCount(BaseCount):
user = models.ForeignKey(UserProfile, on_delete=models.CASCADE)
realm = models.ForeignKey(Realm, on_delete=models.CASCADE)
class Meta:
# Handles invalid duplicate UserCount data
constraints = [
UniqueConstraint(
fields=["user", "property", "subgroup", "end_time"],
condition=Q(subgroup__isnull=False),
name="unique_user_count",
),
UniqueConstraint(
fields=["user", "property", "end_time"],
condition=Q(subgroup__isnull=True),
name="unique_user_count_null_subgroup",
),
]
# This index dramatically improves the performance of
# aggregating from users to realms
indexes = [
models.Index(
fields=["property", "realm", "end_time"],
name="analytics_usercount_property_realm_id_end_time_591dbec1_idx",
)
]
@override
def __str__(self) -> str:
return f"{self.user!r} {self.property} {self.subgroup} {self.value}"
class StreamCount(BaseCount):
stream = models.ForeignKey(Stream, on_delete=models.CASCADE)
realm = models.ForeignKey(Realm, on_delete=models.CASCADE)
class Meta:
# Handles invalid duplicate StreamCount data
constraints = [
UniqueConstraint(
fields=["stream", "property", "subgroup", "end_time"],
condition=Q(subgroup__isnull=False),
name="unique_stream_count",
),
UniqueConstraint(
fields=["stream", "property", "end_time"],
condition=Q(subgroup__isnull=True),
name="unique_stream_count_null_subgroup",
),
]
# This index dramatically improves the performance of
# aggregating from streams to realms
indexes = [
models.Index(
fields=["property", "realm", "end_time"],
name="analytics_streamcount_property_realm_id_end_time_155ae930_idx",
)
]
@override
def __str__(self) -> str:
return f"{self.stream!r} {self.property} {self.subgroup} {self.value} {self.id}"