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

[9.x] Lottery #44894

Merged
merged 5 commits into from Nov 11, 2022
Merged

[9.x] Lottery #44894

merged 5 commits into from Nov 11, 2022

Conversation

timacdonald
Copy link
Member

@timacdonald timacdonald commented Nov 10, 2022

Note This is a re-creation of #42919. It has some community feedback.

tl;dr; ☹️☹️🤑☹️☹️☹️🤑

This PR introduces a new Lottery class. In isolation, this is not super useful, but when used with other features within Laravel and your own application, it can make for a nice experience.

Lotteries can be used in an assortment of ways, for example to do random sampling of an event or assign users to an A/B test group, etc.

Lottery::odds(1, 20)
    ->winner(fn () => $user->group = 'treatment')
    ->loser(fn () => $user->group = 'control')
    ->choose();

A lottery may be created with 2 integer values or a single float.

Lottery::odds(2, 100)->etc();
Lottery::odds(2/100)->etc();

It is also useful with built in framework features...

As the Lottery class is callable, it may be passed directly in as a handler to some of Laravel's features. The following is an example of its use with the new DB::whenQueryingForLongerThan functionality.

// Randomly sample so we don't flood our error handler...

DB::whenQueryingForLongerThan(
    Interval::seconds(2),
    Lottery::odds(1, 100)->winner(fn () => report('DB queries exceeded 2 seconds'))
);

It could also be utilised with the existing n+1 reporting. This is especially useful because unlike the above function, the n+1 handler function may be called thousands of times per request...

// Randomly sample so we don't flood our error handler...

Model::handleLazyLoadingViolationUsing(Lottery::odds(1, 5000)->winner(function ($model, $relation) {
    report(new LazyLoadingViolationException($model, $relation));
}));

You will notice in the above examples that we did not call ->choose(). This is because, an previously mentioned, the Lottery is callable. Additionally you will notice that the closures passed to the lottery receive the arguments $model and $relation.

I wrote a blog post introducing this idea of randomly sampling lazy loading violations via lottery after we implemented this at my $previousGig

Full API examples

/*
 * Via ->choose()
 */

Lottery::odds(1, 10)->choose();

// returns `true` for "win"
// returns `false` for "loss"

Lottery::odds(1, 10)->winner(fn () => 'winner')->choose();

// returns `"winner"` for "win"
// returns `false` for "loss"

Lottery::odds(1, 10)->loser(fn () => 'loser')->choose();

// returns `true` for "win"
// returns `"loser"` for "loss"

Lottery::odds(1, 10)
    ->winner(fn () => 'winner')
    ->loser(fn () => 'loser')
    ->choose();

// returns `"winner"` for "win"
// returns `"loser"` for "loss"

Lottery::odds(1, 5)
    ->winner(fn () => 'winner')
    ->loser(fn () => 'loser')
    ->choose(10);

// example result...
// [
//     "loser",
//     "loser",
//     "loser",
//     "winner",
//     "loser",
//     "winner",
//     "loser",
//     "winner",
//     "loser",
//     "loser",
// ]

/*
 * Via ->__invoke()
 */

// Same as above, however it works different to "choose" as the 
// arguments passed in are passed through to the winner / loser closure.
// This is mostly useful when passing to a function that accepts a callable...

$lottery = Lottery::odds(1, 10)
    ->winner(fn ($arg1, $arg2) => "{$arg1} {$arg2} baz")
    ->loser(fn ($arg1, $arg2) => "{$arg1} {$arg2} qux");

$lottery('foo', 'bar');
// returns `"foo bar baz"` for "win"
// returns `"foo bar qux"` for "loss"

/*
 * Finally...
 */
   
Lottery::setResultFactory(function (): bool {
    // Set your own algorithm for determining results if the built in one doesn't meet your needs.
}); 

This new class could also be utilised in the framework where we perform lotteries, i.e. in the session and database lock garbage collection. I've not included that in this PR as I don't think we need to do it just for the sake of it. This is, after all, primarily a new user-facing feature.

Lottery::odds(...$this->lottery)->winner(
    fn () => $this->connection->table($this->table)->where('expiration', '<=', time())->delete()
)();

I've also added some testing helpers similar to those found in other Laravel support classes.

Lottery::alwaysWin();

// Lottery will now always win.

// -------

Lottery::alwaysWin(function () {
    // Lottery will always win when called within this closure.
});

// -------

Lottery::alwaysLose();

// Lottery will now always lose.

// -------

Lottery::alwaysLose(function () {
    // Lottery will always lose when called within this closure.
});

// -------

Lottery::forceResultWithSequence([true, false]);

// lottery will win then lose, and finally return to random results.

// -------

Lottery::forceResultWithSequence([true, false], function () {
    // what to do when missing items.
});

// lottery will win then lose, and finally call the closure.

// -------

Lottery::determineResultNormally();

// lottery results return to the "normal" method for determining results.

Testing script

collect(Illuminate\Support\Lottery::odds(2, 100)
    ->winner(fn () => ['value' => 'win'])
    ->loser(fn () => ['value' => 'lose'])
    ->choose(1000))
    ->where('value', 'win')
    ->count();

@timacdonald timacdonald changed the title Lottery [9.x] Lottery Nov 10, 2022
/**
* The winning callback.
*
* @var null|callable
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to annotate the callable here and for the looser so that phpstan knows what it accepts and returns? We might have to update the choose docblock then also to mind this.

@timacdonald timacdonald marked this pull request as ready for review November 11, 2022 03:45
@taylorotwell taylorotwell merged commit 3640bb5 into laravel:9.x Nov 11, 2022
@timacdonald timacdonald deleted the lottery branch January 10, 2023 01:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants