Saltar al contenido principal

Qué es la clase Lottery

Illuminate\Support\Lottery es una utilidad que expresa operaciones basadas en probabilidad con una API fluida. Permite describir de forma sencilla patrones como «ejecuta este proceso solo 1 vez cada 100 peticiones» o «registra logs detallados solo en una fracción de las peticiones».
La implementación está en src/Illuminate/Support/Lottery.php. Laravel también la utiliza internamente, por ejemplo en el GC de sesiones o para podar locks de caché.

Uso básico

Indicar la probabilidad mediante enteros

Con Lottery::odds($chances, $outOf) indicas la probabilidad «gana $chances de cada $outOf intentos».
use Illuminate\Support\Lottery;

Lottery::odds(1, 100)           // 1 vez de cada 100
    ->winner(fn () => $this->runMaintenance())
    ->loser(fn () => null)
    ->choose();

Indicar la probabilidad con un decimal

Si omites $outOf y pasas un decimal entre 0.0 y 1.0, se usa directamente como probabilidad.
Lottery::odds(0.01)             // 1 % de probabilidad
    ->winner(fn () => $this->sample())
    ->choose();
Con la especificación decimal, si el valor supera 1.0 se lanza RuntimeException.

Devolver un booleano sin callbacks

Si no configuras winner ni loser, choose() devuelve true si gana y false si pierde.
$shouldSample = Lottery::odds(1, 50)->choose(); // bool

Ejecutar varias veces

Pasando el número de veces a choose($times), se devuelve un array con los resultados.
$results = Lottery::odds(1, 2)
    ->winner(fn () => 'win')
    ->loser(fn () => 'lose')
    ->choose(10);

// p. ej.: ['win', 'lose', 'win', 'win', 'lose', ...]

Pasarlo como callable

Como la instancia de Lottery implementa __invoke, la puedes pasar directamente a APIs que reciban un callable.
// Ejemplo pasándolo como segundo argumento de DB::whenQueryingForLongerThan
DB::whenQueryingForLongerThan(
    Interval::seconds(5),
    Lottery::odds(1, 5)->winner(function ($connection) {
        // Cuando se detecta una consulta lenta, envía alerta con probabilidad 1/5
        Alert::send("Slow query on {$connection->getName()}");
    })
);

Casos de uso prácticos

1. Poda de caché (una vez cada 100)

Ideal para tareas de mantenimiento como el borrado de registros expirados, que no hace falta ejecutar en cada petición.
Lottery::odds(1, 100)
    ->winner(fn () => Cache::store('database')->flush())
    ->choose();

2. Sampling de telemetría (log detallado solo en algunas peticiones)

Cuando registrar el log de todas las peticiones es caro, resulta útil para muestreo.
Lottery::odds(1, 20)
    ->winner(function () use ($request) {
        Log::channel('telemetry')->info('Request sampled', [
            'url'      => $request->url(),
            'duration' => microtime(true) - LARAVEL_START,
            'memory'   => memory_get_peak_usage(true),
        ]);
    })
    ->choose();

3. Comportamiento estilo A/B testing

Reparte a los usuarios entre dos caminos de código de forma probabilística.
$result = Lottery::odds(1, 2)
    ->winner(fn ($user) => $this->newCheckoutFlow($user))
    ->loser(fn ($user) => $this->legacyCheckoutFlow($user))
    ->choose();

4. Ejecución aleatoria de tareas periódicas junto al scheduler

Cuando quieres ejecutar una tarea de vez en cuando evitando duplicados entre varios servidores.
// app/Console/Kernel.php
$schedule->call(function () {
    Lottery::odds(1, 3)
        ->winner(fn () => Artisan::call('cache:prune-stale-tags'))
        ->choose();
})->everyMinute();

Patrones probabilísticos dentro del framework Laravel

Laravel utiliza ampliamente el patrón probabilístico para tareas de mantenimiento internas. Parte de esas implementaciones se escribieron antes de que existiera la clase Lottery, así que usan random_int() directamente, pero comparten la misma idea.
1

Session: garbage collection

Illuminate\Session\Middleware\StartSession::configHitsLottery() utiliza el ajuste lottery de config/session.php y decide con random_int si ejecutar el GC.
// config/session.php
'lottery' => [2, 100], // 2 veces por cada 100 peticiones

// Interior de StartSession (usa random_int directamente)
protected function configHitsLottery(array $config): bool
{
    return random_int(1, $config['lottery'][1]) <= $config['lottery'][0];
}
2

DatabaseLock: poda de locks caducados

Illuminate\Cache\DatabaseLock::acquire() aplica el mismo patrón para eliminar locks expirados en cada adquisición.
// config/cache.php (driver database)
'lock_lottery' => [2, 100], // ejecuta la poda 2 veces por cada 100

// Interior de DatabaseLock::acquire() (usa random_int directamente)
if (random_int(1, $this->lottery[1]) <= $this->lottery[0]) {
    $this->pruneExpiredLocks();
}
3

DB::whenQueryingForLongerThan — ejemplo pasando una Lottery

Como una instancia de Lottery es un callable, se puede pasar directamente como callback de detección de consultas lentas.
DB::whenQueryingForLongerThan(
    Interval::seconds(5),
    Lottery::odds(1, 5)->winner(function ($connection) {
        Log::warning("Slow query on {$connection->getName()}");
    })
);
Frente a Session y DatabaseLock, que usan random_int() directamente, la clase Lottery te permite controlar el resultado durante los tests con alwaysWin() / alwaysLose() / fix(). En el desarrollo de paquetes, optar por Lottery mejora la testabilidad.

Uso en pruebas

Para testear código con aleatoriedad, usa las APIs de test que ofrece Lottery.

Lottery::alwaysWin() — gana siempre

public function test_maintenance_runs_on_win(): void
{
    $ranMaintenance = false;

    Lottery::alwaysWin(function () use (&$ranMaintenance) {
        Lottery::odds(1, 100)
            ->winner(function () use (&$ranMaintenance) {
                $ranMaintenance = true;
            })
            ->choose();
    });

    $this->assertTrue($ranMaintenance);
}

Lottery::alwaysLose() — pierde siempre

public function test_maintenance_skipped_on_lose(): void
{
    $ranMaintenance = false;

    Lottery::alwaysLose(function () use (&$ranMaintenance) {
        Lottery::odds(1, 100)
            ->winner(function () use (&$ranMaintenance) {
                $ranMaintenance = true;
            })
            ->choose();
    });

    $this->assertFalse($ranMaintenance);
}

Lottery::fix() — fija los resultados con una secuencia

Puedes controlar el resultado de varias llamadas con un array de true/false.
public function test_alternating_results(): void
{
    Lottery::fix([true, false, true, false]);

    $results = Lottery::odds(1, 100)
        ->winner(fn () => 'winner')
        ->loser(fn () => 'loser')
        ->choose(4);

    $this->assertSame(['winner', 'loser', 'winner', 'loser'], $results);

    Lottery::determineResultNormally(); // restablece tras el test
}
alwaysWin() / alwaysLose() / fix() alteran una propiedad estática global. Llama siempre a Lottery::determineResultNormally() en el tearDown() de tu test.
protected function tearDown(): void
{
    Lottery::determineResultNormally();

    parent::tearDown();
}

Lottery::setResultFactory() — inyectar una factory personalizada

Si necesitas un control más fino, usa una factory personalizada.
Lottery::setResultFactory(function ($chances, $outOf) {
    // Lógica personalizada que gana siempre
    return true;
});

// Restablece después del test
Lottery::determineResultNormally();

Uso en el desarrollo de paquetes

Registro en el service provider

Si integras una tarea de mantenimiento en el service provider de un paquete, distribuye la carga con Lottery.
use Illuminate\Support\Lottery;
use Illuminate\Support\ServiceProvider;

class AcmeServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $this->app->booted(function () {
            Lottery::odds(1, 100)
                ->winner(fn () => $this->pruneExpiredRecords())
                ->choose();
        });
    }

    protected function pruneExpiredRecords(): void
    {
        $this->app['db']->table('acme_logs')
            ->where('expires_at', '<', now())
            ->delete();
    }
}

Leer las odds desde configuración

Facilita al usuario ajustar la probabilidad exponiéndola en el archivo de configuración.
$lottery = config('acme.prune_lottery', [1, 100]);

Lottery::odds(...$lottery)
    ->winner(fn () => $this->pruneExpiredRecords())
    ->choose();
// config/acme.php
return [
    // [ganadores, intentos] = ejecuta poda 1 vez por cada 100 peticiones
    'prune_lottery' => [1, 100],
];

Sampling en un middleware

use Illuminate\Support\Lottery;

class SampleTelemetryMiddleware
{
    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);

        Lottery::odds(1, 50)
            ->winner(fn () => $this->recordTelemetry($request, $response))
            ->choose();

        return $response;
    }
}

Referencia de la API

MétodoDescripción
Lottery::odds($chances, $outOf)Factory estática que crea una instancia de Lottery.
->winner(callable $callback)Define la callback si gana.
->loser(callable $callback)Define la callback si pierde.
->choose($times = null)Ejecuta la lotería. Con $times devuelve un array.
Lottery::alwaysWin($callback)Para tests: gana siempre.
Lottery::alwaysLose($callback)Para tests: pierde siempre.
Lottery::fix($sequence)Para tests: fija los resultados con una secuencia.
Lottery::determineResultNormally()Restablece la fijación de resultados.
Lottery::setResultFactory(callable)Inyecta lógica personalizada de decisión.

Páginas relacionadas

Trait Macroable

Aprende el patrón de extensión para añadir métodos nuevos a clases existentes.

Trait Conditionable

Aprende cómo diseñar cadenas con bifurcaciones condicionales usando when() / unless().
Última modificación el 13 de julio de 2026