Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions ddpui/migrations/0164_reset_orgfeatureflag_pk_sequence.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""
Data migration to reset the ddpui_orgfeatureflag primary key sequence.

The auto-increment sequence fell out of sync with the table data (likely due
to rows inserted with explicit IDs), causing IntegrityError on INSERT when
the sequence generated an already-used ID.
"""

from django.db import migrations


def reset_sequence(apps, schema_editor):
if schema_editor.connection.vendor == "postgresql":
schema_editor.execute(
"SELECT setval("
" pg_get_serial_sequence('ddpui_orgfeatureflag', 'id'),"
" COALESCE((SELECT MAX(id) FROM ddpui_orgfeatureflag), 1)"
")"
)


class Migration(migrations.Migration):

dependencies = [
("ddpui", "0163_remove_dashboard_root_layout_components"),
]

operations = [
migrations.RunPython(reset_sequence, migrations.RunPython.noop),
]
55 changes: 40 additions & 15 deletions ddpui/utils/feature_flags.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import logging

from django.db import IntegrityError, transaction
from django.db.models import Q
from ddpui.models.org import OrgFeatureFlag, Org

logger = logging.getLogger(__name__)

FEATURE_FLAGS = {
"DATA_QUALITY": "Elementary data quality reports",
"USAGE_DASHBOARD": "Superset usage dashboard for org",
Expand All @@ -12,6 +17,39 @@
}


def _safe_update_or_create(org, flag_name, flag_value):
"""
Wrapper around update_or_create that retries once on IntegrityError.

This handles the case where the PostgreSQL auto-increment sequence for
the primary key is out of sync with the table data (e.g. due to rows
inserted with explicit IDs via fixtures or manual SQL). On the first
attempt the sequence may generate a colliding ID; the retry succeeds
because the sequence advances past the conflict.

The first attempt is wrapped in a savepoint (transaction.atomic) so that
if it fails, the outer transaction is not aborted and the retry can proceed.
"""
try:
with transaction.atomic():
return OrgFeatureFlag.objects.update_or_create(
org=org,
flag_name=flag_name,
defaults={"flag_value": flag_value},
)
except IntegrityError:
logger.warning(
"IntegrityError on OrgFeatureFlag for org=%s flag=%s, retrying",
org,
flag_name,
)
return OrgFeatureFlag.objects.update_or_create(
org=org,
flag_name=flag_name,
defaults={"flag_value": flag_value},
)


def enable_feature_flag(flag_name: str, org: Org = None) -> bool:
"""
Create a flag for the org (or global) in the db if not exists, and enable it.
Expand All @@ -25,14 +63,7 @@ def enable_feature_flag(flag_name: str, org: Org = None) -> bool:
if flag_name not in FEATURE_FLAGS.keys():
return None

flag, created = OrgFeatureFlag.objects.get_or_create(
org=org,
flag_name=flag_name,
defaults={"flag_value": True},
)
if not created:
flag.flag_value = True
flag.save()
_safe_update_or_create(org, flag_name, True)

return True

Expand All @@ -51,13 +82,7 @@ def disable_feature_flag(flag_name: str, org: Org = None) -> bool:
if flag_name not in FEATURE_FLAGS.keys():
return None

try:
flag = OrgFeatureFlag.objects.get(org=org, flag_name=flag_name)
flag.flag_value = False
flag.save()
except OrgFeatureFlag.DoesNotExist:
# create an entry with disabling it
OrgFeatureFlag.objects.create(org=org, flag_name=flag_name, flag_value=False)
_safe_update_or_create(org, flag_name, False)

return True

Expand Down
Loading