Skip to content
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(versioning): ensure that future scheduled changes are migrated to versioning v2 #3958

Merged
Merged
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
5 changes: 4 additions & 1 deletion api/features/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,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 @@ -703,6 +705,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
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
)