11import logging
2+ import warnings
23
34from copy import deepcopy
45from dataclasses import dataclass
1112from typing import Optional
1213from typing import Union
1314
14- import rust_decider # type: ignore
15-
1615from baseplate import RequestContext
1716from baseplate import Span
1817from baseplate .clients import ContextFactory
2322from baseplate .lib .file_watcher import T
2423from baseplate .lib .file_watcher import WatchedFileNotAvailableError
2524from 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
2629from typing_extensions import Literal
2730
2831
@@ -148,19 +151,7 @@ def to_event_dict(self) -> Dict:
148151
149152
150153def 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
166157class 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 ,
0 commit comments