chore(): small improvements
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import hashlib
|
||||
from uuid import UUID
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
@@ -12,6 +13,17 @@ from apps.conflicts.selectors import domain_active_experiments
|
||||
from apps.experiments.models import ACTIVE_STATUSES, Experiment
|
||||
|
||||
|
||||
def _subject_winner_index(
|
||||
*,
|
||||
subject_id: str,
|
||||
domain_id: UUID,
|
||||
size: int,
|
||||
) -> int:
|
||||
seed = f"{domain_id}:{subject_id}".encode()
|
||||
digest = hashlib.sha256(seed).digest()
|
||||
return int.from_bytes(digest[:8], byteorder="big") % size
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def conflict_domain_create(
|
||||
*,
|
||||
@@ -167,7 +179,15 @@ def resolve_domain_conflict(
|
||||
return True
|
||||
|
||||
if domain.policy == ConflictPolicy.MUTUAL_EXCLUSION:
|
||||
winner = active_memberships[0]
|
||||
if subject_id:
|
||||
winner_idx = _subject_winner_index(
|
||||
subject_id=subject_id,
|
||||
domain_id=domain_id,
|
||||
size=len(active_memberships),
|
||||
)
|
||||
winner = active_memberships[winner_idx]
|
||||
else:
|
||||
winner = active_memberships[0]
|
||||
return str(winner.experiment_id) == str(experiment_id)
|
||||
|
||||
if domain.policy == ConflictPolicy.PRIORITY:
|
||||
@@ -187,7 +207,19 @@ def resolve_domain_conflict(
|
||||
tied = [m for m in active_memberships if m.priority == top_priority]
|
||||
if len(tied) <= 1:
|
||||
return True
|
||||
winner = min(tied, key=lambda m: m.experiment.created_at)
|
||||
if subject_id:
|
||||
ordered_tied = sorted(
|
||||
tied,
|
||||
key=lambda m: str(m.experiment_id),
|
||||
)
|
||||
winner_idx = _subject_winner_index(
|
||||
subject_id=subject_id,
|
||||
domain_id=domain_id,
|
||||
size=len(ordered_tied),
|
||||
)
|
||||
winner = ordered_tied[winner_idx]
|
||||
else:
|
||||
winner = min(tied, key=lambda m: m.experiment.created_at)
|
||||
return str(winner.experiment_id) == str(experiment_id)
|
||||
|
||||
return True
|
||||
|
||||
@@ -285,7 +285,7 @@ class ResolveDomainConflictTest(TestCase):
|
||||
exp = experiment_approve(experiment=exp, approver=self.approver)
|
||||
return experiment_start(experiment=exp, user=self.experimenter)
|
||||
|
||||
def test_mutual_exclusion_winner_is_first(self) -> None:
|
||||
def test_mutual_exclusion_deterministic_per_subject(self) -> None:
|
||||
domain = make_domain(
|
||||
suffix="_me",
|
||||
policy=ConflictPolicy.MUTUAL_EXCLUSION,
|
||||
@@ -293,10 +293,17 @@ class ResolveDomainConflictTest(TestCase):
|
||||
)
|
||||
exp1 = self._make_and_start("_me1", domain)
|
||||
exp2 = self._make_and_start("_me2", domain)
|
||||
winner = resolve_domain_conflict(exp1.pk, domain.pk, "u1")
|
||||
self.assertTrue(winner)
|
||||
loser = resolve_domain_conflict(exp2.pk, domain.pk, "u1")
|
||||
self.assertFalse(loser)
|
||||
exp1_u1 = resolve_domain_conflict(exp1.pk, domain.pk, "u1")
|
||||
exp2_u1 = resolve_domain_conflict(exp2.pk, domain.pk, "u1")
|
||||
self.assertNotEqual(exp1_u1, exp2_u1)
|
||||
self.assertEqual(
|
||||
exp1_u1,
|
||||
resolve_domain_conflict(exp1.pk, domain.pk, "u1"),
|
||||
)
|
||||
self.assertEqual(
|
||||
exp2_u1,
|
||||
resolve_domain_conflict(exp2.pk, domain.pk, "u1"),
|
||||
)
|
||||
|
||||
def test_priority_higher_wins(self) -> None:
|
||||
domain = make_domain(
|
||||
@@ -309,7 +316,7 @@ class ResolveDomainConflictTest(TestCase):
|
||||
self.assertTrue(resolve_domain_conflict(exp_high.pk, domain.pk, "u1"))
|
||||
self.assertFalse(resolve_domain_conflict(exp_low.pk, domain.pk, "u1"))
|
||||
|
||||
def test_priority_tie_first_created_wins(self) -> None:
|
||||
def test_priority_tie_deterministic_per_subject(self) -> None:
|
||||
domain = make_domain(
|
||||
suffix="_tie",
|
||||
policy=ConflictPolicy.PRIORITY,
|
||||
@@ -317,8 +324,17 @@ class ResolveDomainConflictTest(TestCase):
|
||||
)
|
||||
exp1 = self._make_and_start("_tie1", domain, priority=5)
|
||||
exp2 = self._make_and_start("_tie2", domain, priority=5)
|
||||
self.assertTrue(resolve_domain_conflict(exp1.pk, domain.pk, "u1"))
|
||||
self.assertFalse(resolve_domain_conflict(exp2.pk, domain.pk, "u1"))
|
||||
exp1_u1 = resolve_domain_conflict(exp1.pk, domain.pk, "u1")
|
||||
exp2_u1 = resolve_domain_conflict(exp2.pk, domain.pk, "u1")
|
||||
self.assertNotEqual(exp1_u1, exp2_u1)
|
||||
self.assertEqual(
|
||||
exp1_u1,
|
||||
resolve_domain_conflict(exp1.pk, domain.pk, "u1"),
|
||||
)
|
||||
self.assertEqual(
|
||||
exp2_u1,
|
||||
resolve_domain_conflict(exp2.pk, domain.pk, "u1"),
|
||||
)
|
||||
|
||||
def test_single_experiment_always_wins(self) -> None:
|
||||
domain = make_domain(suffix="_single")
|
||||
|
||||
Reference in New Issue
Block a user