Skip to content

Commit fe3bb5a

Browse files
committed
minor #19292 [Scheduler] doc draft dynamic schedule (alli83)
This PR was merged into the 6.4 branch. Discussion ---------- [Scheduler] doc draft dynamic schedule following #19244, this section of the documentation focuses on the dynamic aspect of the Schedule: the ability to modify a Schedule by adding or removing recurringMessages. It also introduces another way to create a RecurringMessage using two attributes Commits ------- 33f5017 [Scheduler] doc draft dynamic schedule
2 parents 8bdc50e + 33f5017 commit fe3bb5a

File tree

1 file changed

+220
-20
lines changed

1 file changed

+220
-20
lines changed

scheduler.rst

Lines changed: 220 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,11 @@ Custom Triggers
285285
Custom triggers allow to configure any frequency dynamically. They are created
286286
as services that implement :class:`Symfony\\Component\\Scheduler\\Trigger\\TriggerInterface`.
287287

288+
.. versionadded:: 6.4
289+
290+
Since version 6.4, you can define your messages via a ``callback`` via the
291+
:class:`Symfony\\Component\\Scheduler\\Trigger\\CallbackMessageProvider`.
292+
288293
For example, if you want to send customer reports daily except for holiday periods::
289294

290295
// src/Scheduler/Trigger/NewUserWelcomeEmailHandler.php
@@ -356,10 +361,215 @@ Finally, the recurring messages has to be attached to a schedule::
356361
}
357362
}
358363

359-
.. versionadded:: 6.4
364+
So, this RecurringMessage will encompass both the trigger, defining the generation frequency of the message, and the message itself, the one to be processed by a specific handler.
360365

361-
Since version 6.4, you can define your messages via a ``callback`` via the
362-
:class:`Symfony\\Component\\Scheduler\\Trigger\\CallbackMessageProvider`.
366+
But what is interesting to know is that it also provides you with the ability to generate your message(s) dynamically.
367+
368+
A dynamic vision for the messages generated
369+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
370+
371+
This proves particularly useful when the message depends on data stored in databases or third-party services.
372+
373+
Taking your example of reports generation, it depends on customer requests.
374+
Depending on the specific demands, any number of reports may need to be generated at a defined frequency.
375+
For these dynamic scenarios, it gives you the capability to dynamically define our message(s) instead of statically.
376+
This is achieved by defining a :class:`Symfony\\Component\\Scheduler\\Trigger\\CallbackMessageProvider`.
377+
378+
Essentially, this means you can dynamically, at runtime, define your message(s) through a callback that gets executed each time the scheduler transport checks for messages to be generated::
379+
380+
// src/Scheduler/SaleTaskProvider.php
381+
namespace App\Scheduler;
382+
383+
#[AsSchedule('uptoyou')]
384+
class SaleTaskProvider implements ScheduleProviderInterface
385+
{
386+
public function getSchedule(): Schedule
387+
{
388+
return $this->schedule ??= (new Schedule())
389+
->with(
390+
RecurringMessage::trigger(
391+
new ExcludeHolidaysTrigger(
392+
CronExpressionTrigger::fromSpec('@daily'),
393+
),
394+
// instead of being static as in the previous example
395+
new CallbackMessageProvider([$this, 'generateReports'], 'foo')),
396+
RecurringMessage::cron(‘3 8 * * 1’, new CleanUpOldSalesReport())
397+
398+
);
399+
}
400+
401+
public function generateReports(MessageContext $context)
402+
{
403+
// ...
404+
yield new SendDailySalesReports();
405+
yield new ReportSomethingReportSomethingElse();
406+
....
407+
}
408+
}
409+
410+
Exploring alternatives for crafting your Recurring Messages
411+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
412+
413+
There is also another way to build a RecurringMessage, and this can be done simply by adding an attribute above a service or a command:
414+
:class:`Symfony\\Component\\Scheduler\\Attribute\\AsPeriodicTask` attribute and :class:`Symfony\\Component\\Scheduler\\Attribute\\AsCronTask` attribute.
415+
416+
For both of these attributes, you have the ability to define the schedule to roll with using the ``schedule``option. By default, the ``default`` named schedule will be used.
417+
Also, by default, the ``__invoke`` method of your service will be called but, it's also possible to specify the method to call via the ``method``option and you can define arguments via ``arguments``option if necessary.
418+
419+
The distinction between these two attributes lies in the options pertaining to the trigger:
420+
421+
#. :class:`Symfony\\Component\\Scheduler\\Attribute\\AsPeriodicTask` attribute:
422+
423+
#. You can configure various options such as ``frequencies``, ``from``, ``until`` and ``jitter``, encompassing options related to the trigger.
424+
425+
#. :class:`Symfony\\Component\\Scheduler\\Attribute\\AsCronTask` attribute:
426+
427+
#. You can configure various options such as ``expression``, ``jitter``, encompassing options related to the trigger.
428+
429+
By defining one of these two attributes, it enables the execution of your service or command, considering all the options that have been specified within the attributes.
430+
431+
Managing Scheduled Messages
432+
---------------------------
433+
434+
Modifying Scheduled Messages in real time
435+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
436+
437+
While planning a schedule in advance is beneficial, it is rare for a schedule to remain static over time.
438+
After a certain period, some RecurringMessages may become obsolete, while others may need to be integrated into our planning.
439+
440+
As a general practice, to alleviate a heavy workload, the recurring messages in the schedules are stored in memory to avoid recalculation each time the scheduler transport generates messages.
441+
However, this approach can have a flip side.
442+
443+
In the context of our sales company, certain promotions may occur during specific periods and need to be communicated repetitively throughout a given timeframe
444+
or the deletion of old reports needs to be halted under certain circumstances.
445+
446+
This is why the Scheduler incorporates a mechanism to dynamically modify the schedule and consider all changes in real-time.
447+
448+
Strategies for adding, removing, and modifying entries within the Schedule
449+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
450+
451+
The schedule provides you with the ability to :method:`Symfony\\Component\\Scheduler\Schedule::add`, :method:`Symfony\\Component\\Scheduler\Schedule::remove`, or :method:`Symfony\\Component\\Scheduler\Schedule::clear` all associated recurring messages,
452+
resulting in the reset and recalculation of the in-memory stack of recurring messages.
453+
454+
For instance, for various reasons, if there's no need to generate a report, a callback can be employed to conditionally skip generating of some or all reports.
455+
456+
However, if the intention is to completely remove a recurring message and its recurrence,
457+
the :class:`Symfony\\Component\\Scheduler\Schedule` offers a :method:`Symfony\\Component\\Scheduler\Schedule::remove` or a :method:`Symfony\\Component\\Scheduler\Schedule::removeById` method.
458+
This can be particularly useful in your case, especially if you need to halt the generation of the recurring message, which involves deleting old reports.
459+
460+
In your handler, you can check a condition and, if affirmative, access the :class:`Symfony\\Component\\Scheduler\Schedule` and invoke this method::
461+
462+
// src/Scheduler/SaleTaskProvider.php
463+
namespace App\Scheduler;
464+
465+
#[AsSchedule('uptoyou')]
466+
class SaleTaskProvider implements ScheduleProviderInterface
467+
{
468+
public function getSchedule(): Schedule
469+
{
470+
$this->removeOldReports = RecurringMessage::cron(‘3 8 * * 1’, new CleanUpOldSalesReport());
471+
472+
return $this->schedule ??= (new Schedule())
473+
->with(
474+
// ...
475+
$this->removeOldReports;
476+
);
477+
}
478+
479+
// ...
480+
481+
public function removeCleanUpMessage()
482+
{
483+
$this->getSchedule()->getSchedule()->remove($this->removeOldReports);
484+
}
485+
}
486+
487+
// src/Scheduler/Handler/.php
488+
namespace App\Scheduler\Handler;
489+
490+
#[AsMessageHandler]
491+
class CleanUpOldSalesReportHandler
492+
{
493+
public function __invoke(CleanUpOldSalesReport $cleanUpOldSalesReport): void
494+
{
495+
// do what you have to do
496+
497+
if ($isFinished) {
498+
$this->mySchedule->removeCleanUpMessage();
499+
}
500+
}
501+
}
502+
503+
Nevertheless, this system may not be the most suitable for all scenarios. Also, the handler should ideally be designed to process the type of message it is intended for,
504+
without making decisions about adding or removing a new recurring message.
505+
506+
For instance, if, due to an external event, there is a need to add a recurrent message aimed at deleting reports,
507+
it can be challenging to achieve within the handler. This is because the handler will no longer be called or executed once there are no more messages of that type.
508+
509+
However, the Scheduler also features an event system that is integrated into a Symfony full-stack application by grafting onto Symfony Messenger events.
510+
These events are dispatched through a listener, providing a convenient means to respond.
511+
512+
Managing Scheduled Messages via Events
513+
--------------------------------------
514+
515+
A strategic event handling
516+
~~~~~~~~~~~~~~~~~~~~~~~~~~
517+
518+
The goal is to provide flexibility in deciding when to take action while preserving decoupling.
519+
Three primary event types have been introduced types
520+
521+
#. PRE_RUN_EVENT
522+
523+
#. POST_RUN_EVENT
524+
525+
#. FAILURE_EVENT
526+
527+
Access to the schedule is a crucial feature, allowing effortless addition or removal of message types.
528+
Additionally, it will be possible to access the currently processed message and its message context.
529+
530+
In consideration of our scenario, you can easily listen to the PRE_RUN_EVENT and check if a certain condition is met.
531+
532+
For instance, you might decide to add a recurring message for cleaning old reports again, with the same or different configurations, or add any other recurring message(s).
533+
534+
If you had chosen to handle the deletion of the recurring message, you could have easily done so in a listener for this event.
535+
536+
Importantly, it reveals a specific feature :method:`Symfony\\Component\\Scheduler\\Event\\PreRunEvent::shouldCancel` that allows you to prevent the message of the deleted recurring message from being transferred and processed by its handler::
537+
538+
// src/Scheduler/SaleTaskProvider.php
539+
namespace App\Scheduler;
540+
541+
#[AsSchedule('uptoyou')]
542+
class SaleTaskProvider implements ScheduleProviderInterface
543+
{
544+
public function getSchedule(): Schedule
545+
{
546+
$this->removeOldReports = RecurringMessage::cron(‘3 8 * * 1’, new CleanUpOldSalesReport());
547+
548+
return $this->schedule ??= (new Schedule())
549+
->with(
550+
// ...
551+
);
552+
->before(function(PreRunEvent $event) {
553+
$message = $event->getMessage();
554+
$messageContext = $event->getMessageContext();
555+
556+
// can access the schedule
557+
$schedule = $event->getSchedule()->getSchedule();
558+
559+
// can target directly the RecurringMessage being processed
560+
$schedule->removeById($messageContext->id);
561+
562+
//Allow to call the ShouldCancel() and avoid the message to be handled
563+
$event->shouldCancel(true);
564+
}
565+
->after(function(PostRunEvent $event) {
566+
// Do what you want
567+
}
568+
->onFailure(function(FailureEvent $event) {
569+
// Do what you want
570+
}
571+
}
572+
}
363573

364574
Consuming Messages (Running the Worker)
365575
---------------------------------------
@@ -408,31 +618,21 @@ recurring messages. You can narrow down the list to a specific schedule:
408618
# use the --all option to also display the terminated recurring messages
409619
$ php bin/console --all
410620
411-
.. versionadded:: 6.4
412-
413-
The ``--date`` and ``--all`` options were introduced in Symfony 6.4.
414-
415621
Efficient management with Symfony Scheduler
416622
-------------------------------------------
417623

418-
When a worker is restarted or undergoes shutdown for a period, the Scheduler
419-
transport won't be able to generate the messages (because they are created
420-
on-the-fly by the scheduler transport). This implies that any messages
421-
scheduled to be sent during the worker's inactive period are not sent, and the
422-
Scheduler will lose track of the last processed message. Upon restart, it will
423-
recalculate the messages to be generated from that point onward.
624+
When a worker is restarted or undergoes shutdown for a period, the Scheduler transport won't be able to generate the messages (because they are created on-the-fly by the scheduler transport).
625+
This implies that any messages scheduled to be sent during the worker's inactive period are not sent, and the Scheduler will lose track of the last processed message.
626+
Upon restart, it will recalculate the messages to be generated from that point onward.
424627

425-
To illustrate, consider a recurring message set to be sent every 3 days. If a
426-
worker is restarted on day 2, the message will be sent 3 days from the restart,
427-
on day 5.
628+
To illustrate, consider a recurring message set to be sent every 3 days.
629+
If a worker is restarted on day 2, the message will be sent 3 days from the restart, on day 5.
428630

429-
While this behavior may not necessarily pose a problem, there is a possibility
430-
that it may not align with what you are seeking.
631+
While this behavior may not necessarily pose a problem, there is a possibility that it may not align with what you are seeking.
431632

432633
That's why the scheduler allows to remember the last execution date of a message
433634
via the ``stateful`` option (and the :doc:`Cache component </components/cache>`).
434-
This allows the system to retain the state of the schedule, ensuring that when
435-
a worker is restarted, it resumes from the point it left off::
635+
This allows the system to retain the state of the schedule, ensuring that when a worker is restarted, it resumes from the point it left off.::
436636

437637
// src/Scheduler/SaleTaskProvider.php
438638
namespace App\Scheduler;

0 commit comments

Comments
 (0)