Home

Awesome

Chaotic Schedule

Laravel package for randomizing command schedule intervals via pRNGs.

Reliability Rating Maintainability Rating Duplicated Lines (%)

codecov Quality Gate Status

DeepSource DeepSource

Packagist: https://packagist.org/packages/skywarth/chaotic-schedule

Table of Contents

<a name='installation'></a>

Installation

  1. Consider the requirements

    • PHP >=7.4 is required
  2. Install the package via composer:

composer require skywarth/chaotic-schedule
  1. (optional) Publish the config in order to customize it
 php artisan vendor:publish --provider "Skywarth\ChaoticSchedule\Providers\ChaoticScheduleServiceProvider" --tag="config"
  1. Done. You may now use random time and date macros on schedules

<a name='problem-definition'></a>

Problem Definition

Ever wanted to run your scheduled commands on random times of the day, or on certain days of the week? Or you may need to send some notifications not on fixed date times, but rather on random intervals hence it feels more human. Then this is the package you're looking for.

This Laravel packages enables you to run commands on random intervals and periods while respecting the boundaries set exclusively by you.

<a name='use-cases'></a>

Use Cases

<a name='documentation'></a>

Documentation

<a name='how-to-use'></a>

<a name='random-time-macros'></a>

Random Time Macros

<a name='at-random'></a>

1. ->atRandom(string $minTime, string $maxTime,?string $uniqueIdentifier=null,?callable $closure=null)

Used for scheduling your commands to run at random time of the day.

ParameterTypeExample ValueDescription
minTimestring'14:15'Minimum value for the random time range (inclusive)
maxTimestring'22:38'Maximum value for the random time range (inclusive)
uniqueIdentifierstring (nullable)'my-custom-identifier'Custom identifier that will be used for determining seed for the given command. If null/default provided, command's signature will be used for this. It is primarily used for distinguishing randomization of same command schedules.
closurecallable (nullable)<pre>function(int $motd){<br><br>return $motd+5;<br>}<br></pre>Optional closure to tweak the designated random minute of the day according to your needs. For example you may use this to run the command only on odd-numbered minutes. int minute of the day and Event (Schedule) instance is injected, meanwhile int response is expected from the closure.

Run a command daily on a random time between 08:15 and 11:42

$schedule->command('your-command-signature:here')->daily()->atRandom('08:15','11:42');

Run a command every Tuesday, Saturday and Sunday on a random time between 04:20 and 06:09

$schedule->command('your-command-signature:here')->days([Schedule::TUESDAY, Schedule::SATURDAY, Schedule::SUNDAY])->atRandom('04:20','06:09');

Run a command every Sunday between 16:00 - 17:00 and also on Monday between 09:00 - 12:00

Notice the unique identifier parameter

//Observe that the both schedules share the same command, but one has custom unique identifier
$schedule->command('your-command-signature:here')->sundays()->atRandom('16:00','17:00');
$schedule->command('your-command-signature:here')->mondays()->atRandom('09:00','12:00','this-is-special');
//Since the latter has a unique identifier, it has a distinguished seed which completely differentiates the generated randoms.

Run a command weekdays at a random time between 12:00 and 20:00, but only if the hour is not 15:00.

$schedule->command('your-command-signature:here')->weekdays()->atRandom('16:00', '17:00', null, function(int $motd){
  if($motd>=900 && $motd<=960){//$motd represents minute-of-the-day. 900th minute is 15:00. 
    return $motd+60;
  }else{
    return $motd;     
  }
});

<a name='daily-at-random'></a>

2. ->dailyAtRandom(string $minTime, string $maxTime,?string $uniqueIdentifier=null,?callable $closure=null)

Identical to atRandom macro. Just a different name.

<a name='hourly-at-random'></a>

3. ->hourlyAtRandom(int $minMinutes=0, int $maxMinutes=59,?string $uniqueIdentifier=null,?callable $closure=null)

Used for scheduling you commands to run every hour at random minutes.

ParameterTypeExample ValueDescription
minMinutesint15Minimum value for the random minute of hour (inclusive)
maxMinutesint44Maximum value for the random minute of hour (inclusive)
uniqueIdentifierstring (nullable)'my-custom-identifier'Custom identifier that will be used for determining seed for the given command. If null/default provided, command's signature will be used for this. It is primarily used for distinguishing randomization of same command schedules.
closurecallable (nullable)<pre>function(int $randomMinute, Event $schedule){<br><br>return $randomMinute%10;<br>}<br></pre>Optional closure to tweak the designated random minute according to your needs. For example you may use this to run the command only on multiplies of 10. <br><br> Generated int random minute (between 0-59) and Event (Schedule) instance is injected, meanwhile int response that is between 0-59 is expected from the closure.

Run a command every hour between 15th and 25th minutes randomly.

$schedule->command('your-command-signature:here')->hourlyAtRandom(15,25);

Run a command every hour twice, once between 0-12 minute mark, another between 48-59 minute mark.

$schedule->command('your-command-signature:here')->hourlyAtRandom(0,12);
$schedule->command('your-command-signature:here')->hourlyAtRandom(48,59,'custom-identifier-to-customize-seed');

Run a command every hour, between minutes 30-45 but only on multiplies of 5.

$schedule->command('your-command-signature:here')->hourlyAtRandom(30,45,null,function(int $minute, Event $schedule){
return min(($minute%5),0);
});

<a name='hourly-multiple-at-random'></a>

4. ->hourlyMultipleAtRandom(int $minMinutes=0, int $maxMinutes=59, int $timesMin=1, int $timesMax=1, ?string $uniqueIdentifier=null,?callable $closure=null)

Similar to ->hourlyAtRandom, it is used for scheduling your commands to run every hour on random minutes. Difference between this and ->hourlyAtRandom is: ->hourlyMultipleAtRandom allows you to run a command multiple times per hour.

Example use case: I want to run a command every hour, 1-5 times at random, on random minutes. E.g. run minutes:[5,11,32,44]

ParameterTypeExample ValueDescription
minMinutesint15Minimum value for the random minute of hour (inclusive)
maxMinutesint44Maximum value for the random minute of hour (inclusive)
timesMinint3Minimum amount of times to run this command per hour (inclusive). E.g: $timesMin=3, $timesMax=10, the command will run at least 3, at the most 10 times per hour. Run amounts decided per hour basis.
timesMaxint10Maximum amount of times to run this command per hour (inclusive). E.g: $timesMin=5, $timesMax=17, the command will run at least 5, at the most 17 times per hour. Run amounts decided per hour basis.
uniqueIdentifierstring (nullable)'my-custom-identifier'Custom identifier that will be used for determining seed for the given command. If null/default provided, command's signature will be used for this. It is primarily used for distinguishing randomization/seeding of same command schedules.
closurecallable (nullable)<pre>function(Collection $minutes,Event $e){<br><br> return $minutes->diff([4,8,15,16,23,42])<br>->values();<br> };<br></pre>Optional closure to tweak the designated random run minutes according to your needs. For example you may use this to run the command only on those minutes which are not in an array. <br><br> Designated random run minutes Collection that consist of int minutes (between 0-59) and Event (Schedule) instance is injected, meanwhile Collection response that contains int minutes between 0-59 is expected from the closure.

Run a command 4-5 (random) times per hour, only on weekdays (constant, every day), between 08:00 and 18:00 (constant, every hour between these). Minutes of each hour are random.

https://www.reddit.com/r/laravel/comments/18v714l/comment/ktkyc72/?utm_source=share&utm_medium=web2x&context=3

$schedule->command('your-command-signature:here')->hourlyMultipleAtRandom(0,59,4,5)->weekdays()->between('08:00','18:00');

Run a command exactly 8 times an hour, it should run only between 20-40 minute marks, run only on wednesdays.

$schedule->command('your-command-signature:here')->hourlyMultipleAtRandom(20,40,8,8)->wednesdays();

Run a command 2-6 times an hour, it should run only between 10-48 minute marks, run only on; tuesday,thursday,saturday, it should run only on even(divisible by 2) minutes.

$schedule->command('your-command-signature:here')->hourlyMultipleAtRandom(10,48,2,6,null,function(Collection $designatedMinutes,Event $event){
    return $designatedMinutes->map(function(int $minute){
        return $minute-(($minute%2));//rounding numbers to closest even number, if the number is odd
    });
})->days([Schedule::TUESDAY, Schedule::THURSDAY,Schedule::SATURDAY])();

<a name='random-date-macros'></a>

Random Date Macros

1. ->randomDays(int $periodType, ?array $daysOfTheWeek, int $timesMin, int $timesMax, ?string $uniqueIdentifier=null,?callable $closure=null)

Used for scheduling your commands to run at random dates for given constraints and period.

ParameterTypeExample ValueDescription
periodTypeintRandomDateScheduleBasis::WeekThe most crucial parameter for random date scheduling. It defines the period of the random date range, seed basis/consistency and generated random dates. It defines the seed for the random dates, so for the given period your randoms stay consistent. You may use any value presented in RandomDateScheduleBasis class/enum.
daysOfWeekarray<int> (nullable)[Carbon::Sunday, Carbon::Tuesday]Days of the week that will be used for random date generation. Only those days you pass will be picked and used. For example: if you pass [Carbon::Wednesday, Carbon:: Monday], random dates will be only on wednesdays and mondays. Since it is optional, if you don't pass anything for it that means all days of the week will be available to be used.
timesMinint2Defines the minimum amount of times the command is expected to run for the given period. E.g: period is week and timesMin=4, that means this command will run at least 4 times each week.
timesMaxint12Defines the maximum amount of times the command is expected to run for the given period. E.g: period is month and timesMin=5 and timesMax=12, that means this command will run at least 5, at most 12 times each month. Exact number of times that it'll run is resolved in runtime according to seed.
uniqueIdentifierstring (nullable)'my-custom-identifier'Custom identifier that will be used for determining seed for the given command. If null/default provided, command's signature will be used for this. It is primarily used for distinguishing randomization of same command schedules.
closurecallable (nullable)<pre>function(Collection $possibleDates, Event $schedule){<br><br>return $possibleDates->filter(function (Carbon $date){<br/><br/> return $date->day%2!==0;//odd numbered days only <br/>});<br>}<br></pre>Closure parameter for adjusting random dates for the command. <br> This closure is especially useful if you would like to exclude certain dates, or add some dates to the possible dates to choose from. <br><br> Possible dates as Carbon instances are injected as collection to the closures, these dates represent the pool of possible dates to choose from for random dates, it doesn't represent designated run dates. Event (Schedule) instance is injected as well. Closure response is expected to be a collection of Carbon instances.

Run a command 5 to 10 times/days (as in dates) each month randomly.

$schedule->command('your-command-signature:here')->randomDays(RandomDateScheduleBasis::MONTH,[],5,10);

Run a command exactly 2 times (as in dates) per week, but only on wednesdays or saturdays.

$schedule->command('your-command-signature:here')->randomDays(RandomDateScheduleBasis::WEEK,[Carbon::WEDNESDAY,Carbon::SATURDAY],2,2);

Run a command 15-30 times (as in dates) per year, only on Fridays.

$schedule->command('your-command-signature:here')->randomDays(RandomDateScheduleBasis::YEAR,[Carbon::FRIDAY],15,30);

Run a command 1 to 3 times (as in dates) per month, only on weekends, and only on odd days .

$schedule->command('your-command-signature:here')->randomDays(
    RandomDateScheduleBasis::MONTH,
    [Carbon::SATURDAY,Carbon::SUNDAY],
    1,3,
    null,
    function (Collection $dates){
        return $dates->filter(function (Carbon $date){
            return $date->day%2!==0;//odd numbered days only
        });
    }
);

Joint examples

Examples about using both random time and random date macros together.

Run a command 1 to 2 times (as in dates) among Friday, Tuesday, Sunday, and only between 14:48 - 16:54

$schedule->command('your-command-signature:here')->weekly()->randomDays(RandomDateScheduleBasis::WEEK,[Carbon::FRIDAY,Carbon::Tuesday,Carbon::Sunday],1,2)->atRandom('14:48','16:54');

<a name='info-for-nerds'></a>

Info for nerds

<a name='consistency-seed-prng'></a>

Consistency, seed and pRNG

It was a concern to generate consistent and same random values for the duration of the given interval. This is due to the fact that the Laravel scheduler is triggered via CRON tab every minute. So we needed a solution to generate consistent randoms for each trigger of the scheduler. Otherwise, it would simply designate random date/time runs each time it runs, which will result in: commands never running at all (because run designation changes constantly), or commands running more that desired/planned.

In the world of cryptography and statistics, such challenges are tackled via pRNGs (pseudo random number generators). pRNGs as indicated in its name: is a pseudo random number generator which works with seed values and generates randoms determined by that seed, hence the name. Therefore, pRNGs would allow us to generate consistent and exactly same random values as long as seed remains the same. Now the question is what seed shall we pair pRNG with? After some pondering around, I deduced that if I give corresponding date/time of the interval, it would effectively be consistent throughout the interval. Henceforth, all the randomizing methods and macros work by utilizing SeedGenerationService which is responsible for generating seeds based on certain intervals (day, month, week etc.) This ensures your random run date/times are consistent throughout the interval, enabling consistency.

To those with a keen eye, this might present a possible problem. If the seed is paired only with interval, wouldn't that result in identical random date/time runs for separate commands? Exactly! Because of this, SeedGenerationService also takes command signatures into consideration by incorporating uniqueIdentifier (which is either command signature or custom identifier) into it's seed designation methods. This way, even if the separate commands have identical random scheduling, they'll have distinct randomization for them thanks to the uniqueIdentifier

<a name='asserting-the-chaos'></a>

Asserting the chaos

When dealing with pRNGs, nothing is truly chaotic and random, actually. It's all mathematics and statistics, it's deterministic. In order to ensure no harm could come from these randoms, I've prepared dozens of unit and feature tests for the different aspects of the library. From seed generation to generated random consistency, from distribution uniformity to validations, from design pattern implementation to dependency injection, all is well tested and asserted. See the code coverage reports and CI/CD runs regarding these functional tests.

<a name='performance'></a>

Performance

As you might already know, Laravel scheduler runs every minute via CRON tab entry (see: https://laravel.com/docs/10.x/scheduling#running-the-scheduler). And since kernel.php (where you define your schedules) runs every minute, Laravel has to determine whether each command is designated to run at this minute or not. This is performed by running & checking datetime scheduling assignments and ->when() statements on your command scheduling.

This library heavily relies on pRNG, seed generation based on date/time and run designations. Hence, it is no surprise these calculations, randomization (pseudo) and designations are performed each time your kernel.php runs, which is every minute as explained on the previous paragraph. So yes, if you're on low-specs, it could affect your pipeline. Because these seed determination, run date/time designations, pseudo-random values are determined on each schedule iteration. Massive amounts (250+) of randomized command schedules could clog your server performance a bit in terms of memory usage.

But other than that, as the Jules from Pulp Fiction said:

"As far as I know, MF is tip-top"

<a name='roadmap-and-todos'></a>

Roadmap & TODOs

<a name='credits-and-references'></a>

Credits & References

RNGs

This project has been developed using JetBrains products. Thanks for their support for open-source development.

<img width="150" src="https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg" alt="JetBrains Logo (Main) logo."><img width="200" src="https://resources.jetbrains.com/storage/products/company/brand/logos/PhpStorm.svg" alt="PhpStorm logo.">