Skip to content

Commit

Permalink
fix(versioning): ensure that future scheduled changes are migrated to…
Browse files Browse the repository at this point in the history
… versioning v2 (#3958)
  • Loading branch information
matthewelwell authored May 16, 2024
1 parent 5e7ea36 commit c5aa610
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 3 deletions.
5 changes: 4 additions & 1 deletion api/features/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -625,7 +625,9 @@ def clone(
)

clone.environment = env
clone.version = None if as_draft else version or self.version
clone.version = (
None if as_draft or environment_feature_version else version or self.version
)
clone.live_from = live_from
clone.environment_feature_version = environment_feature_version
clone.save()
Expand Down Expand Up @@ -732,6 +734,7 @@ def get_multivariate_feature_state_value(
def check_for_duplicate_feature_state(self):
if self.version is None:
return

filter_ = Q(
environment=self.environment,
feature=self.feature,
Expand Down
27 changes: 25 additions & 2 deletions api/features/versioning/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from django.utils import timezone

from features.models import FeatureState
from features.versioning.models import EnvironmentFeatureVersion
from features.versioning.schemas import (
EnvironmentFeatureVersionWebhookDataSerializer,
Expand Down Expand Up @@ -83,15 +84,37 @@ def _create_initial_feature_versions(environment: "Environment"):
)

latest_feature_states = get_environment_flags_queryset(
environment=environment
).filter(identity__isnull=True, feature=feature)
environment=environment, feature_name=feature.name
).filter(identity__isnull=True)
related_feature_segments = FeatureSegment.objects.filter(
feature_states__in=latest_feature_states
)

latest_feature_states.update(environment_feature_version=ef_version)
related_feature_segments.update(environment_feature_version=ef_version)

scheduled_feature_states = FeatureState.objects.filter(
live_from__gt=now,
change_request__isnull=False,
change_request__committed_at__isnull=False,
change_request__deleted_at__isnull=True,
).select_related("change_request")
for feature_state in scheduled_feature_states:
ef_version = EnvironmentFeatureVersion.objects.create(
feature=feature,
environment=environment,
published_at=feature_state.change_request.committed_at,
live_from=feature_state.live_from,
change_request=feature_state.change_request,
)
feature_state.environment_feature_version = ef_version
feature_state.change_request = None

FeatureState.objects.bulk_update(
scheduled_feature_states,
fields=["environment_feature_version", "change_request"],
)


@register_task_handler()
def trigger_update_version_webhooks(environment_feature_version_uuid: str) -> None:
Expand Down
74 changes: 74 additions & 0 deletions api/tests/unit/features/versioning/test_unit_versioning_tasks.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
from datetime import timedelta

import freezegun
from django.utils import timezone
from pytest_mock import MockerFixture

from environments.identities.models import Identity
Expand All @@ -12,6 +16,7 @@
from features.versioning.versioning_service import (
get_environment_flags_queryset,
)
from features.workflows.core.models import ChangeRequest
from projects.models import Project
from segments.models import Segment
from users.models import FFAdminUser
Expand Down Expand Up @@ -167,3 +172,72 @@ def test_trigger_update_version_webhooks(
},
event_type=WebhookEventType.NEW_VERSION_PUBLISHED,
)


def test_enable_v2_versioning_for_scheduled_changes(
environment: Environment, staff_user: FFAdminUser, feature: Feature
) -> None:
# Given
now = timezone.now()
one_from_from_now = now + timedelta(hours=1)
two_hours_from_now = now + timedelta(hours=2)

# The current environment feature state for the provided feature
current_environment_feature_state = FeatureState.objects.get(
environment=environment, feature=feature
)

# A feature state scheduled to go live in the future that is published
scheduled_change_request = ChangeRequest.objects.create(
environment=environment, title="Scheduled Change", user=staff_user
)
scheduled_feature_state = FeatureState.objects.create(
feature=feature,
enabled=True,
environment=environment,
live_from=one_from_from_now,
change_request=scheduled_change_request,
version=None,
)
scheduled_change_request.commit(staff_user)

# and a feature state scheduled to go live in the future that is not published (and hence
# shouldn't affect anything)
unpublished_scheduled_change_request = ChangeRequest.objects.create(
environment=environment, title="Unpublished Scheduled Change", user=staff_user
)
FeatureState.objects.create(
feature=feature,
enabled=True,
environment=environment,
live_from=two_hours_from_now,
change_request=unpublished_scheduled_change_request,
version=None,
)

# When
enable_v2_versioning(environment.id)

# Then
environment_flags_queryset_now = get_environment_flags_queryset(environment)
assert environment_flags_queryset_now.count() == 1
assert environment_flags_queryset_now.first() == current_environment_feature_state

with freezegun.freeze_time(one_from_from_now):
environment_flags_queryset_one_hour_later = get_environment_flags_queryset(
environment
)
assert environment_flags_queryset_one_hour_later.count() == 1
assert (
environment_flags_queryset_one_hour_later.first() == scheduled_feature_state
)

with freezegun.freeze_time(two_hours_from_now):
environment_flags_queryset_two_hours_later = get_environment_flags_queryset(
environment
)
assert environment_flags_queryset_two_hours_later.count() == 1
assert (
environment_flags_queryset_two_hours_later.first()
== scheduled_feature_state
)

0 comments on commit c5aa610

Please sign in to comment.