pub struct ExperimentService { /* private fields */ }Expand description
The main experiment service.
Wrap an ExperimentStore (for persistence) and an optional
ExposureSink (default: TracingExposureSink). The service is cheaply
clone-able and intended to be stored as an AppState extension.
§Example
use autumn_web::experiments::{
ExperimentConfig, ExperimentService, InMemoryExperimentStore, VariantConfig,
};
use std::sync::Arc;
let store = Arc::new(InMemoryExperimentStore::new());
let svc = ExperimentService::new(store);
svc.create(ExperimentConfig::new("onboarding_v3", vec![
VariantConfig::new("control", 50),
VariantConfig::new("wizard", 50),
])).unwrap();
svc.start("onboarding_v3").unwrap();
let variant = svc.assign("onboarding_v3", "user:42").unwrap();
assert!(matches!(variant.as_str(), "control" | "wizard"));Implementations§
Source§impl ExperimentService
impl ExperimentService
Sourcepub fn new(store: Arc<dyn ExperimentStore>) -> Self
pub fn new(store: Arc<dyn ExperimentStore>) -> Self
Create a new service backed by store, with the default
TracingExposureSink.
Sourcepub fn with_exposure_sink(self, sink: Arc<dyn ExposureSink>) -> Self
pub fn with_exposure_sink(self, sink: Arc<dyn ExposureSink>) -> Self
Override the default ExposureSink.
Sourcepub fn assign(
&self,
experiment: &str,
actor: &str,
) -> Result<String, ExperimentError>
pub fn assign( &self, experiment: &str, actor: &str, ) -> Result<String, ExperimentError>
Assign a variant to actor in experiment.
Assignment rules (evaluated in order):
Archived→Err(Archived).Concluded→Ok(winner)without emitting an exposure.Draft→Err(NotRunning).- Staff/QA override present → return override variant (exposure emitted,
tagged
is_override = true). - Sticky assignment already recorded → return cached variant (exposure emitted each call).
- Mutual exclusion check → if
actorhas any assignment in a sibling experiment in the same group, returnErr(ExcludedByGroup). - Compute bucket → select variant by weight → store sticky → emit exposure.
§Errors
ExperimentError::NotFound— no such experiment.ExperimentError::Archived— experiment is archived.ExperimentError::NotRunning— experiment is inDraftstate.ExperimentError::ExcludedByGroup— actor excluded by mutual exclusion.ExperimentError::NoVariant— all variant weights are zero.ExperimentError::Store— backend failure.
Sourcepub fn assign_with_request_id(
&self,
experiment: &str,
actor: &str,
request_id: Option<&str>,
) -> Result<String, ExperimentError>
pub fn assign_with_request_id( &self, experiment: &str, actor: &str, request_id: Option<&str>, ) -> Result<String, ExperimentError>
Sourcepub fn create(&self, config: ExperimentConfig) -> Result<(), ExperimentError>
pub fn create(&self, config: ExperimentConfig) -> Result<(), ExperimentError>
Declare a new experiment (starts in Draft state).
§Errors
Returns ExperimentError::Store on backend failure.
Sourcepub fn start(&self, name: &str) -> Result<(), ExperimentError>
pub fn start(&self, name: &str) -> Result<(), ExperimentError>
Transition a Draft or Concluded experiment to Running.
Archived experiments are terminal and cannot be restarted.
§Errors
Returns ExperimentError::NotFound if the experiment is unknown.
Returns ExperimentError::Archived if the experiment is archived.
Sourcepub fn conclude(&self, name: &str, winner: &str) -> Result<(), ExperimentError>
pub fn conclude(&self, name: &str, winner: &str) -> Result<(), ExperimentError>
Conclude a running experiment, pinning winner as the result.
After concluding, assign() returns winner for all actors without
emitting new exposure events.
§Errors
Returns ExperimentError::NotFound if the experiment is unknown.
Sourcepub fn archive(&self, name: &str) -> Result<(), ExperimentError>
pub fn archive(&self, name: &str) -> Result<(), ExperimentError>
Archive an experiment.
Archived experiments reject all new assignments with
ExperimentError::Archived.
§Errors
Returns ExperimentError::NotFound if the experiment is unknown.
Sourcepub fn set_weights(
&self,
name: &str,
variants: Vec<VariantConfig>,
actor: Option<&str>,
) -> Result<(), ExperimentError>
pub fn set_weights( &self, name: &str, variants: Vec<VariantConfig>, actor: Option<&str>, ) -> Result<(), ExperimentError>
Update the variant weights for name.
Existing sticky assignments are not re-bucketed: already-assigned actors keep their variant. New actors are bucketed against the updated weights.
§Errors
Returns ExperimentError::NotFound if the experiment is unknown.
Sourcepub fn set_override(
&self,
experiment: &str,
actor: &str,
variant: &str,
) -> Result<(), ExperimentError>
pub fn set_override( &self, experiment: &str, actor: &str, variant: &str, ) -> Result<(), ExperimentError>
Pin actor to variant in experiment, bypassing weight-based bucketing.
Overrides are used by staff/QA to force a specific variant during manual
testing. Exposure events include is_override: true.
§Errors
Returns ExperimentError::NotFound if the experiment is unknown.
Sourcepub fn list(&self) -> Result<Vec<ExperimentConfig>, ExperimentError>
pub fn list(&self) -> Result<Vec<ExperimentConfig>, ExperimentError>
Sourcepub fn status(&self, name: &str) -> Result<ExperimentConfig, ExperimentError>
pub fn status(&self, name: &str) -> Result<ExperimentConfig, ExperimentError>
Return the current configuration for name.
§Errors
Returns ExperimentError::NotFound if the experiment is unknown.
Sourcepub fn history(
&self,
experiment: &str,
limit: usize,
) -> Result<Vec<ChangeRecord>, ExperimentError>
pub fn history( &self, experiment: &str, limit: usize, ) -> Result<Vec<ChangeRecord>, ExperimentError>
Return the change log for experiment (most-recent first), capped at limit.
§Errors
Returns ExperimentError::Store on backend failure.
Trait Implementations§
Source§impl Clone for ExperimentService
impl Clone for ExperimentService
Source§fn clone(&self) -> ExperimentService
fn clone(&self) -> ExperimentService
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreAuto Trait Implementations§
impl !RefUnwindSafe for ExperimentService
impl !UnwindSafe for ExperimentService
impl Freeze for ExperimentService
impl Send for ExperimentService
impl Sync for ExperimentService
impl Unpin for ExperimentService
impl UnsafeUnpin for ExperimentService
Blanket Implementations§
Source§impl<T> AggregateExpressionMethods for T
impl<T> AggregateExpressionMethods for T
Source§fn aggregate_distinct(self) -> Self::Outputwhere
Self: DistinctDsl,
fn aggregate_distinct(self) -> Self::Outputwhere
Self: DistinctDsl,
DISTINCT modifier for aggregate functions Read moreSource§fn aggregate_all(self) -> Self::Outputwhere
Self: AllDsl,
fn aggregate_all(self) -> Self::Outputwhere
Self: AllDsl,
ALL modifier for aggregate functions Read moreSource§fn aggregate_filter<P>(self, f: P) -> Self::Output
fn aggregate_filter<P>(self, f: P) -> Self::Output
Source§fn aggregate_order<O>(self, o: O) -> Self::Outputwhere
Self: OrderAggregateDsl<O>,
fn aggregate_order<O>(self, o: O) -> Self::Outputwhere
Self: OrderAggregateDsl<O>,
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>, which can then be
downcast into Box<dyn ConcreteType> where ConcreteType implements Trait.Source§fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
Rc<Trait> (where Trait: Downcast) to Rc<Any>, which can then be further
downcast into Rc<ConcreteType> where ConcreteType implements Trait.Source§fn as_any(&self) -> &(dyn Any + 'static)
fn as_any(&self) -> &(dyn Any + 'static)
&Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &Any’s vtable from &Trait’s.Source§fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.Source§impl<T> DowncastSend for T
impl<T> DowncastSend for T
Source§impl<T> DowncastSync for T
impl<T> DowncastSync for T
impl<A, B, T> HttpServerConnExec<A, B> for Twhere
B: Body,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
Source§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
Source§impl<T> IntoSql for T
impl<T> IntoSql for T
Source§fn into_sql<T>(self) -> Self::Expression
fn into_sql<T>(self) -> Self::Expression
self to an expression for Diesel’s query builder. Read moreSource§fn as_sql<'a, T>(&'a self) -> <&'a Self as AsExpression<T>>::Expression
fn as_sql<'a, T>(&'a self) -> <&'a Self as AsExpression<T>>::Expression
&self to an expression for Diesel’s query builder. Read moreSource§impl<T> Pointable for T
impl<T> Pointable for T
Source§impl<T> PolicyExt for Twhere
T: ?Sized,
impl<T> PolicyExt for Twhere
T: ?Sized,
impl<T> Read<Exclusive, BecauseExclusive> for Twhere
T: ?Sized,
Source§impl<T> RepositoryHooksClone for Twhere
T: Clone,
impl<T> RepositoryHooksClone for Twhere
T: Clone,
Source§fn autumn_clone(&self) -> T
fn autumn_clone(&self) -> T
Source§impl<T, Conn> RunQueryDsl<Conn> for T
impl<T, Conn> RunQueryDsl<Conn> for T
Source§fn execute<'conn, 'query>(
self,
conn: &'conn mut Conn,
) -> <Conn as AsyncConnectionCore>::ExecuteFuture<'conn, 'query>
fn execute<'conn, 'query>( self, conn: &'conn mut Conn, ) -> <Conn as AsyncConnectionCore>::ExecuteFuture<'conn, 'query>
Source§fn load<'query, 'conn, U>(
self,
conn: &'conn mut Conn,
) -> AndThen<Self::LoadFuture<'conn>, TryCollect<Self::Stream<'conn>, Vec<U>>>
fn load<'query, 'conn, U>( self, conn: &'conn mut Conn, ) -> AndThen<Self::LoadFuture<'conn>, TryCollect<Self::Stream<'conn>, Vec<U>>>
Source§fn load_stream<'conn, 'query, U>(
self,
conn: &'conn mut Conn,
) -> Self::LoadFuture<'conn>where
Conn: AsyncConnectionCore,
U: 'conn,
Self: LoadQuery<'query, Conn, U> + 'query,
fn load_stream<'conn, 'query, U>(
self,
conn: &'conn mut Conn,
) -> Self::LoadFuture<'conn>where
Conn: AsyncConnectionCore,
U: 'conn,
Self: LoadQuery<'query, Conn, U> + 'query,
Stream] with the returned rows. Read moreSource§fn get_result<'query, 'conn, U>(
self,
conn: &'conn mut Conn,
) -> AndThen<Self::LoadFuture<'conn>, LoadNext<Pin<Box<Self::Stream<'conn>>>>>
fn get_result<'query, 'conn, U>( self, conn: &'conn mut Conn, ) -> AndThen<Self::LoadFuture<'conn>, LoadNext<Pin<Box<Self::Stream<'conn>>>>>
Source§fn get_results<'query, 'conn, U>(
self,
conn: &'conn mut Conn,
) -> AndThen<Self::LoadFuture<'conn>, TryCollect<Self::Stream<'conn>, Vec<U>>>
fn get_results<'query, 'conn, U>( self, conn: &'conn mut Conn, ) -> AndThen<Self::LoadFuture<'conn>, TryCollect<Self::Stream<'conn>, Vec<U>>>
Vec with the affected rows. Read moreSource§impl<T> Scoped for T
impl<T> Scoped for T
Source§fn scope(ctx: &PolicyContext) -> ScopeQuery<'_, Self>
fn scope(ctx: &PolicyContext) -> ScopeQuery<'_, Self>
ScopeQuery for this type. Resolves the
registered scope at .load() time, not here.