Skip to content

Commit 6da1657

Browse files
authored
Merge pull request #79 from reddit/use_RustDecider
Use `rust_decider.RustDecider` in `get_variant()` & `get_variant_without_expose()`
2 parents 5c72f76 + da6e6c6 commit 6da1657

File tree

6 files changed

+204
-150
lines changed

6 files changed

+204
-150
lines changed

reddit_decider/__init__.py

Lines changed: 60 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import warnings
23

34
from copy import deepcopy
45
from dataclasses import dataclass
@@ -11,8 +12,6 @@
1112
from typing import Optional
1213
from typing import Union
1314

14-
import rust_decider # type: ignore
15-
1615
from baseplate import RequestContext
1716
from baseplate import Span
1817
from baseplate.clients import ContextFactory
@@ -23,6 +22,10 @@
2322
from baseplate.lib.file_watcher import T
2423
from baseplate.lib.file_watcher import WatchedFileNotAvailableError
2524
from reddit_edgecontext import ValidatedAuthenticationToken
25+
from rust_decider import Decider as RustDecider
26+
from rust_decider import DeciderException
27+
from rust_decider import FeatureNotFoundException
28+
from rust_decider import make_ctx
2629
from typing_extensions import Literal
2730

2831

@@ -148,19 +151,7 @@ def to_event_dict(self) -> Dict:
148151

149152

150153
def init_decider_parser(file: IO) -> Any:
151-
return rust_decider.init(
152-
"darkmode overrides targeting holdout mutex_group fractional_availability value", file.name
153-
)
154-
155-
156-
def validate_decider(decider: Optional[Any]) -> None:
157-
if decider is None:
158-
logger.error("Rust decider is None--did not initialize.")
159-
160-
if decider:
161-
decider_err = decider.err()
162-
if decider_err:
163-
logger.error(f"Rust decider has initialization error: {decider_err}")
154+
return RustDecider(file.name)
164155

165156

166157
class Decider:
@@ -174,13 +165,13 @@ class Decider:
174165
def __init__(
175166
self,
176167
decider_context: DeciderContext,
177-
config_watcher: FileWatcher,
168+
internal: Optional[RustDecider],
178169
server_span: Span,
179170
context_name: str,
180171
event_logger: Optional[EventLogger] = None,
181172
):
182173
self._decider_context = decider_context
183-
self._config_watcher = config_watcher
174+
self._internal = internal
184175
self._span = server_span
185176
self._context_name = context_name
186177
if event_logger:
@@ -189,27 +180,22 @@ def __init__(
189180
self._event_logger = DebugLogger()
190181

191182
def _get_decider(self) -> Optional[T]:
192-
try:
193-
decider = self._config_watcher.get_data()
194-
validate_decider(decider)
195-
return decider
196-
except WatchedFileNotAvailableError as exc:
197-
logger.error("Experiment config file unavailable: %s", str(exc))
198-
except TypeError as exc:
199-
logger.error("Could not load experiment config: %s", str(exc))
183+
if self._internal is not None:
184+
return self._internal.get_decider()
185+
200186
return None
201187

202188
def _get_ctx(self) -> Any:
203189
context_fields = self._decider_context.to_dict()
204-
return rust_decider.make_ctx(context_fields)
190+
return make_ctx(context_fields)
205191

206192
def _get_ctx_with_set_identifier(
207193
self, identifier: str, identifier_type: Literal["user_id", "device_id", "canonical_url"]
208194
) -> Dict[str, Any]:
209195
context_fields = self._decider_context.to_dict()
210196
context_fields[identifier_type] = identifier
211197

212-
return rust_decider.make_ctx(context_fields)
198+
return make_ctx(context_fields)
213199

214200
def _format_decision(self, decision_dict: Dict[str, str]) -> Dict[str, Any]:
215201
out = {}
@@ -352,32 +338,28 @@ def get_variant(
352338
353339
:return: Variant name if a variant is assigned, :code:`None` otherwise.
354340
"""
355-
decider = self._get_decider()
356-
if decider is None:
341+
if self._internal is None:
342+
logger.error("RustDecider is None--did not initialize.")
357343
return None
358344

359-
ctx = self._get_ctx()
360-
ctx_err = ctx.err()
361-
if ctx_err is not None:
362-
logger.info(f"Encountered error in rust_decider.make_ctx(): {ctx_err}")
363-
return None
345+
ctx = self._decider_context.to_dict()
364346

365-
choice = decider.choose(experiment_name, ctx)
366-
error = choice.err()
367-
368-
if error:
369-
logger.info(f"Encountered error in decider.choose(): {error}")
347+
try:
348+
decision = self._internal.choose(experiment_name, ctx)
349+
except FeatureNotFoundException as exc:
350+
warnings.warn(exc)
351+
return None
352+
except DeciderException as exc:
353+
logger.info(exc)
370354
return None
371-
372-
variant = choice.decision()
373355

374356
event_context_fields = self._decider_context.to_event_dict()
375357
event_context_fields.update(exposure_kwargs or {})
376358

377-
for event in choice.events():
359+
for event in decision.events:
378360
self._send_expose(event=event, exposure_fields=event_context_fields)
379361

380-
return variant
362+
return decision.variant
381363

382364
def get_variant_without_expose(self, experiment_name: str) -> Optional[str]:
383365
"""Return a bucketing variant, if any, without emitting exposure event.
@@ -394,32 +376,27 @@ def get_variant_without_expose(self, experiment_name: str) -> Optional[str]:
394376
395377
:return: Variant name if a variant is assigned, None otherwise.
396378
"""
397-
decider = self._get_decider()
398-
if decider is None:
399-
return None
400-
401-
ctx = self._get_ctx()
402-
ctx_err = ctx.err()
403-
if ctx_err is not None:
404-
logger.info(f"Encountered error in rust_decider.make_ctx(): {ctx_err}")
379+
if self._internal is None:
380+
logger.error("RustDecider is None--did not initialize.")
405381
return None
406382

407-
choice = decider.choose(experiment_name, ctx)
408-
error = choice.err()
383+
ctx = self._decider_context.to_dict()
409384

410-
if error:
411-
logger.info(f"Encountered error in decider.choose(): {error}")
385+
try:
386+
decision = self._internal.choose(experiment_name, ctx)
387+
except FeatureNotFoundException as exc:
388+
warnings.warn(exc)
389+
return None
390+
except DeciderException as exc:
391+
logger.info(exc)
412392
return None
413-
414-
variant = choice.decision()
415393

416394
event_context_fields = self._decider_context.to_event_dict()
417395

418-
# expose Holdout if the experiment is part of one
419-
for event in choice.events():
396+
for event in decision.events:
420397
self._send_expose_if_holdout(event=event, exposure_fields=event_context_fields)
421398

422-
return variant
399+
return decision.variant
423400

424401
def expose(
425402
self, experiment_name: str, variant_name: str, **exposure_kwargs: Optional[Dict[str, Any]]
@@ -1026,30 +1003,30 @@ def _prune_extracted_dict(extracted_dict: dict) -> dict:
10261003
return parsed_extracted_fields
10271004

10281005
def _minimal_decider(
1029-
self, name: str, span: Span, parsed_extracted_fields: Optional[Dict] = None
1006+
self,
1007+
internal: Optional[RustDecider],
1008+
name: str,
1009+
span: Span,
1010+
parsed_extracted_fields: Optional[Dict] = None,
10301011
) -> Decider:
10311012
return Decider(
10321013
decider_context=DeciderContext(extracted_fields=parsed_extracted_fields),
1033-
config_watcher=self._filewatcher,
1014+
internal=internal,
10341015
server_span=span,
10351016
context_name=name,
10361017
event_logger=self._event_logger,
10371018
)
10381019

10391020
def make_object_for_context(self, name: str, span: Span) -> Decider:
1040-
decider = None
1021+
rs_decider = None
10411022
try:
1042-
decider = self._filewatcher.get_data()
1023+
rs_decider = self._filewatcher.get_data()
10431024
except WatchedFileNotAvailableError as exc:
1044-
logger.error("Experiment config file unavailable: %s", str(exc))
1045-
except TypeError as exc:
1046-
logger.error("Could not load experiment config: %s", str(exc))
1047-
1048-
validate_decider(decider)
1025+
logger.error(f"Experiment config file unavailable: {exc}")
10491026

10501027
if span is None:
10511028
logger.debug("`span` is `None` in reddit_decider `make_object_for_context()`.")
1052-
return self._minimal_decider(name=name, span=span)
1029+
return self._minimal_decider(internal=rs_decider, name=name, span=span)
10531030

10541031
request = None
10551032
parsed_extracted_fields = None
@@ -1072,21 +1049,30 @@ def make_object_for_context(self, name: str, span: Span) -> Decider:
10721049
# if `edge_context` is inaccessible, bail early
10731050
if request is None:
10741051
return self._minimal_decider(
1075-
name=name, span=span, parsed_extracted_fields=parsed_extracted_fields
1052+
internal=rs_decider,
1053+
name=name,
1054+
span=span,
1055+
parsed_extracted_fields=parsed_extracted_fields,
10761056
)
10771057

10781058
ec = request.edge_context
10791059

10801060
if ec is None:
10811061
return self._minimal_decider(
1082-
name=name, span=span, parsed_extracted_fields=parsed_extracted_fields
1062+
internal=rs_decider,
1063+
name=name,
1064+
span=span,
1065+
parsed_extracted_fields=parsed_extracted_fields,
10831066
)
10841067
except Exception as exc:
10851068
logger.info(
10861069
f"Unable to access `request.edge_context` in `make_object_for_context()`. details: {exc}"
10871070
)
10881071
return self._minimal_decider(
1089-
name=name, span=span, parsed_extracted_fields=parsed_extracted_fields
1072+
internal=rs_decider,
1073+
name=name,
1074+
span=span,
1075+
parsed_extracted_fields=parsed_extracted_fields,
10901076
)
10911077

10921078
# All fields below are derived from `edge_context`
@@ -1189,7 +1175,7 @@ def make_object_for_context(self, name: str, span: Span) -> Decider:
11891175

11901176
return Decider(
11911177
decider_context=decider_context,
1192-
config_watcher=self._filewatcher,
1178+
internal=rs_decider,
11931179
server_span=span,
11941180
context_name=name,
11951181
event_logger=self._event_logger,

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
-r requirements-transitive.txt
22
baseplate==2.0.0a1
33
black==21.4b2
4-
reddit-decider==1.2.24
4+
reddit-decider==1.2.28
55
flake8==3.9.1
66
mypy==0.790
77
pyramid==2.0 # required for `from baseplate.frameworks.pyramid import BaseplateRequest` which calls `import pyramid.events`

setup.cfg

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,8 @@ warn_unused_ignores = True
2424
warn_return_any = False
2525
no_implicit_reexport = True
2626
strict_equality = True
27+
28+
[mypy-rust_decider]
29+
# TODO: add stubs to reddit-decider
30+
# see https://pyo3.rs/v0.16.2/python_typing_hints.html
31+
ignore_missing_imports = True

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
install_requires=[
2020
"baseplate>=2.0.0a1,<3.0",
2121
"reddit-edgecontext>=1.0.0a3,<2.0",
22-
"reddit-decider~=1.2.24",
22+
"reddit-decider~=1.2.28",
2323
"typing_extensions>=3.10.0.0,<5.0",
2424
],
2525
package_data={"reddit_experiments": ["py.typed"]},

0 commit comments

Comments
 (0)