-
Notifications
You must be signed in to change notification settings - Fork 299
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
Fix duplicate orders on routes and escalation policies #2568
Changes from all commits
174132a
2dce985
97b23a2
ebf9df8
dc3e169
ecc0457
02a3776
c0b8541
7c4fa65
2118323
80c43dc
6fcd51a
0642b17
3b531ee
5a27101
7520963
7ad4284
2d169a2
ef336bc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
# Generated by Django 3.2.20 on 2023-07-18 09:53 | ||
|
||
from django.db import migrations, models | ||
import django_migration_linter as linter | ||
from django.db.models import Count | ||
|
||
from common.database import get_random_readonly_database_key_if_present_otherwise_default | ||
|
||
|
||
def fix_duplicate_orders(apps, schema_editor): | ||
EscalationPolicy = apps.get_model('alerts', 'EscalationPolicy') | ||
|
||
# it should be safe to use a readonly database because duplicates are pretty infrequent | ||
db = get_random_readonly_database_key_if_present_otherwise_default() | ||
|
||
# find all (escalation_chain_id, order) tuples that have more than one entry (meaning duplicates) | ||
items_with_duplicate_orders = EscalationPolicy.objects.using(db).values( | ||
"escalation_chain_id", "order" | ||
).annotate(count=Count("order")).order_by().filter(count__gt=1) # use order_by() to reset any existing ordering | ||
|
||
# make sure we don't fix the same escalation chain more than once | ||
escalation_chain_ids = set(item["escalation_chain_id"] for item in items_with_duplicate_orders) | ||
|
||
for escalation_chain_id in escalation_chain_ids: | ||
policies = EscalationPolicy.objects.filter(escalation_chain_id=escalation_chain_id).order_by("order", "id") | ||
# assign correct sequential order for each policy starting from 0 | ||
for idx, policy in enumerate(policies): | ||
policy.order = idx | ||
EscalationPolicy.objects.bulk_update(policies, fields=["order"]) | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
('alerts', '0023_auto_20230718_0952'), | ||
] | ||
|
||
operations = [ | ||
linter.IgnoreMigration(), # adding a unique constraint after fixing duplicates should be fine | ||
migrations.AlterModelOptions( | ||
name='escalationpolicy', | ||
options={'ordering': ['order']}, | ||
), | ||
migrations.AlterField( | ||
model_name='escalationpolicy', | ||
name='order', | ||
field=models.PositiveIntegerField(db_index=True, editable=False, null=True), | ||
), | ||
migrations.RunPython(fix_duplicate_orders, migrations.RunPython.noop), | ||
migrations.AddConstraint( | ||
model_name='escalationpolicy', | ||
constraint=models.UniqueConstraint(fields=('escalation_chain_id', 'order'), name='unique_escalation_policy_order'), | ||
), | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
# Generated by Django 3.2.20 on 2023-07-18 10:42 | ||
|
||
from django.db import migrations, models | ||
import django_migration_linter as linter | ||
from django.db.models import Count | ||
|
||
from common.database import get_random_readonly_database_key_if_present_otherwise_default | ||
|
||
|
||
def fix_duplicate_orders(apps, schema_editor): | ||
ChannelFilter = apps.get_model('alerts', 'ChannelFilter') | ||
|
||
# it should be safe to use a readonly database because duplicates are pretty infrequent | ||
db = get_random_readonly_database_key_if_present_otherwise_default() | ||
|
||
# find all (alert_receive_channel_id, is_default, order) tuples that have more than one entry (meaning duplicates) | ||
items_with_duplicate_orders = ChannelFilter.objects.using(db).values( | ||
"alert_receive_channel_id", "is_default", "order" | ||
).annotate(count=Count("order")).order_by().filter(count__gt=1) # use order_by() to reset any existing ordering | ||
|
||
# make sure we don't fix the same (alert_receive_channel_id, is_default) pair more than once | ||
values_to_fix = set((item["alert_receive_channel_id"], item["is_default"]) for item in items_with_duplicate_orders) | ||
|
||
for alert_receive_channel_id, is_default in values_to_fix: | ||
channel_filters = ChannelFilter.objects.filter( | ||
alert_receive_channel_id=alert_receive_channel_id, is_default=is_default | ||
).order_by("order", "id") | ||
# assign correct sequential order for each route starting from 0 | ||
for idx, channel_filter in enumerate(channel_filters): | ||
channel_filter.order = idx | ||
ChannelFilter.objects.bulk_update(channel_filters, fields=["order"]) | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
('alerts', '0024_auto_20230718_0953'), | ||
] | ||
|
||
operations = [ | ||
linter.IgnoreMigration(), # adding a unique constraint after fixing duplicates should be fine | ||
migrations.AlterModelOptions( | ||
name='channelfilter', | ||
options={'ordering': ['alert_receive_channel_id', 'is_default', 'order']}, | ||
), | ||
migrations.AlterField( | ||
model_name='channelfilter', | ||
name='order', | ||
field=models.PositiveIntegerField(db_index=True, editable=False, null=True), | ||
), | ||
migrations.RunPython(fix_duplicate_orders, migrations.RunPython.noop), | ||
migrations.AddConstraint( | ||
model_name='channelfilter', | ||
constraint=models.UniqueConstraint(fields=('alert_receive_channel_id', 'is_default', 'order'), name='unique_channel_filter_order'), | ||
), | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,10 +6,10 @@ | |
from django.conf import settings | ||
from django.core.validators import MinLengthValidator | ||
from django.db import models | ||
from ordered_model.models import OrderedModel | ||
|
||
from common.jinja_templater import apply_jinja_template | ||
from common.jinja_templater.apply_jinja_template import JinjaTemplateError, JinjaTemplateWarning | ||
from common.ordered_model.ordered_model import OrderedModel | ||
from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length | ||
|
||
logger = logging.getLogger(__name__) | ||
|
@@ -34,7 +34,7 @@ class ChannelFilter(OrderedModel): | |
Actually it's a Router based on terms now. Not a Filter. | ||
""" | ||
|
||
order_with_respect_to = ("alert_receive_channel", "is_default") | ||
order_with_respect_to = ["alert_receive_channel_id", "is_default"] | ||
|
||
public_primary_key = models.CharField( | ||
max_length=20, | ||
|
@@ -82,11 +82,12 @@ class ChannelFilter(OrderedModel): | |
is_default = models.BooleanField(default=False) | ||
|
||
class Meta: | ||
ordering = ( | ||
"alert_receive_channel", | ||
"is_default", | ||
"order", | ||
) | ||
ordering = ["alert_receive_channel_id", "is_default", "order"] | ||
constraints = [ | ||
models.UniqueConstraint( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adding a unique constraint to enforce |
||
fields=["alert_receive_channel_id", "is_default", "order"], name="unique_channel_filter_order" | ||
) | ||
] | ||
|
||
def __str__(self): | ||
return f"{self.pk}: {self.filtering_term or 'default'}" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,8 +3,8 @@ | |
from django.conf import settings | ||
from django.core.validators import MinLengthValidator | ||
from django.db import models | ||
from ordered_model.models import OrderedModel | ||
|
||
from common.ordered_model.ordered_model import OrderedModel | ||
from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length | ||
|
||
|
||
|
@@ -23,7 +23,7 @@ def generate_public_primary_key_for_escalation_policy(): | |
|
||
|
||
class EscalationPolicy(OrderedModel): | ||
order_with_respect_to = "escalation_chain" | ||
order_with_respect_to = ["escalation_chain_id"] | ||
|
||
MAX_TIMES_REPEAT = 5 | ||
|
||
|
@@ -312,6 +312,12 @@ class EscalationPolicy(OrderedModel): | |
num_alerts_in_window = models.PositiveIntegerField(null=True, default=None) | ||
num_minutes_in_window = models.PositiveIntegerField(null=True, default=None) | ||
|
||
class Meta: | ||
ordering = ["order"] | ||
constraints = [ | ||
models.UniqueConstraint(fields=["escalation_chain_id", "order"], name="unique_escalation_policy_order") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adding a unique constraint to enforce |
||
] | ||
|
||
def __str__(self): | ||
return f"{self.pk}: {self.step_type_verbal}" | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,12 +7,12 @@ | |
from apps.telegram.models import TelegramToOrganizationConnector | ||
from common.api_helpers.custom_fields import OrganizationFilteredPrimaryKeyRelatedField | ||
from common.api_helpers.exceptions import BadRequest | ||
from common.api_helpers.mixins import EagerLoadingMixin, OrderedModelSerializerMixin | ||
from common.api_helpers.mixins import EagerLoadingMixin | ||
from common.jinja_templater.apply_jinja_template import JinjaTemplateError | ||
from common.utils import is_regex_valid | ||
|
||
|
||
class ChannelFilterSerializer(OrderedModelSerializerMixin, EagerLoadingMixin, serializers.ModelSerializer): | ||
class ChannelFilterSerializer(EagerLoadingMixin, serializers.ModelSerializer): | ||
id = serializers.CharField(read_only=True, source="public_primary_key") | ||
alert_receive_channel = OrganizationFilteredPrimaryKeyRelatedField(queryset=AlertReceiveChannel.objects) | ||
escalation_chain = OrganizationFilteredPrimaryKeyRelatedField( | ||
|
@@ -27,7 +27,6 @@ class ChannelFilterSerializer(OrderedModelSerializerMixin, EagerLoadingMixin, se | |
queryset=TelegramToOrganizationConnector.objects, filter_field="organization", allow_null=True, required=False | ||
) | ||
telegram_channel_details = serializers.SerializerMethodField() | ||
order = serializers.IntegerField(required=False) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
filtering_term_as_jinja2 = serializers.SerializerMethodField() | ||
filtering_term = serializers.CharField(required=False, allow_null=True, allow_blank=True) | ||
|
||
|
@@ -37,7 +36,6 @@ class Meta: | |
model = ChannelFilter | ||
fields = [ | ||
"id", | ||
"order", | ||
"alert_receive_channel", | ||
"escalation_chain", | ||
"slack_channel", | ||
|
@@ -148,7 +146,6 @@ class Meta: | |
model = ChannelFilter | ||
fields = [ | ||
"id", | ||
"order", | ||
"alert_receive_channel", | ||
"escalation_chain", | ||
"slack_channel", | ||
|
@@ -181,14 +178,8 @@ def to_representation(self, obj): | |
return result | ||
|
||
def create(self, validated_data): | ||
order = validated_data.pop("order", None) | ||
if order is not None: | ||
alert_receive_channel_id = validated_data.get("alert_receive_channel") | ||
self._validate_order(order, {"alert_receive_channel_id": alert_receive_channel_id, "is_default": False}) | ||
instance = super().create(validated_data) | ||
self._change_position(order, instance) | ||
else: | ||
instance = super().create(validated_data) | ||
instance = super().create(validated_data) | ||
instance.to_index(0) # the new route should be the first one | ||
return instance | ||
|
||
|
||
|
@@ -200,18 +191,8 @@ class Meta(ChannelFilterCreateSerializer.Meta): | |
extra_kwargs = {"filtering_term": {"required": False}} | ||
|
||
def update(self, instance, validated_data): | ||
order = validated_data.get("order") | ||
filtering_term = validated_data.get("filtering_term") | ||
|
||
if instance.is_default and order is not None and instance.order != order: | ||
raise BadRequest(detail="The order of default channel filter cannot be changed") | ||
|
||
if instance.is_default and filtering_term is not None: | ||
raise BadRequest(detail="Filtering term of default channel filter cannot be changed") | ||
|
||
if order is not None: | ||
self._validate_order( | ||
order, {"alert_receive_channel_id": instance.alert_receive_channel_id, "is_default": False} | ||
) | ||
self._change_position(order, instance) | ||
return super().update(instance, validated_data) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -85,7 +85,6 @@ class Meta: | |
model = EscalationPolicy | ||
fields = [ | ||
"id", | ||
"order", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
"step", | ||
"wait_delay", | ||
"escalation_chain", | ||
|
@@ -101,7 +100,6 @@ class Meta: | |
"notify_to_group", | ||
"important", | ||
] | ||
read_only_fields = ["order"] | ||
|
||
SELECT_RELATED = [ | ||
"escalation_chain", | ||
|
@@ -199,7 +197,6 @@ def _convert_to_important_step_if_needed(validated_data): | |
|
||
class EscalationPolicyCreateSerializer(EscalationPolicySerializer): | ||
class Meta(EscalationPolicySerializer.Meta): | ||
read_only_fields = ["order"] | ||
extra_kwargs = {"escalation_chain": {"required": True, "allow_null": False}} | ||
|
||
def create(self, validated_data): | ||
|
@@ -212,7 +209,7 @@ class EscalationPolicyUpdateSerializer(EscalationPolicySerializer): | |
escalation_chain = serializers.CharField(read_only=True, source="escalation_chain.public_primary_key") | ||
|
||
class Meta(EscalationPolicySerializer.Meta): | ||
read_only_fields = ["order", "escalation_chain"] | ||
read_only_fields = ["escalation_chain"] | ||
|
||
def update(self, instance, validated_data): | ||
step = validated_data.get("step", instance.step) | ||
|
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.
Off-by-one bug making
executed_escalation_policy_snapshots
smaller than it should be (it probably affects the escalation auditor task)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.
good catch 👍 thanks for patching this