Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit f130187

Browse files
committed
[Scheduler] doc draft dynamic schedule
1 parent 69c1e78 commit f130187

File tree

1 file changed

+220
-17
lines changed

1 file changed

+220
-17
lines changed

scheduler.rst

Lines changed: 220 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -164,10 +164,6 @@ It uses the same syntax as the `cron command-line utility`_::
164164
// optionally you can define the timezone used by the cron expression
165165
RecurringMessage::cron('* * * * *', new Message(), new \DateTimeZone('Africa/Malabo'));
166166

167-
.. versionadded:: 6.4
168-
169-
The feature to define the cron timezone was introduced in Symfony 6.4.
170-
171167
Before using it, you have to install the following dependency:
172168

173169
.. code-block:: terminal
@@ -271,6 +267,216 @@ Finally, the recurring messages has to be attached to a schedule::
271267
}
272268
}
273269

270+
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.
271+
272+
But what is interesting to know is that it also provides you with the ability to generate your message(s) dynamically.
273+
274+
A dynamic vision for the messages generated
275+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
276+
277+
This proves particularly useful when the message depends on data stored in databases or third-party services.
278+
279+
Taking your example of reports generation, it depends on customer requests.
280+
Depending on the specific demands, any number of reports may need to be generated at a defined frequency.
281+
For these dynamic scenarios, it gives you the capability to dynamically define our message(s) instead of statically.
282+
This is achieved by defining a :class:`Symfony\\Component\\Scheduler\\Trigger\\CallbackMessageProvider`.
283+
284+
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::
285+
286+
// src/Scheduler/MyScheduleProvider.php
287+
namespace App\Scheduler;
288+
289+
#[AsSchedule('uptoyou')]
290+
class SaleTaskProvider implements ScheduleProviderInterface
291+
{
292+
public function getSchedule(): Schedule
293+
{
294+
return $this->schedule ??= (new Schedule())
295+
->with(
296+
RecurringMessage::trigger(
297+
new ExcludeHolidaysTrigger(
298+
CronExpressionTrigger::fromSpec('@daily'),
299+
),
300+
// instead of being static as in the previous example
301+
new CallbackMessageProvider([$this, 'generateReports'], 'foo')),
302+
RecurringMessage::cron(‘3 8 * * 1’, new CleanUpOldSalesReport())
303+
304+
);
305+
}
306+
307+
public function generateReports(MessageContext $context)
308+
{
309+
// ...
310+
yield new SendDailySalesReports();
311+
yield new ReportSomethingReportSomethingElse();
312+
....
313+
}
314+
}
315+
316+
Exploring alternatives for crafting your Recurring Messages
317+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
318+
319+
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:
320+
:class:`Symfony\\Component\\Scheduler\\Attribute\\AsPeriodicTask` attribute and :class:`Symfony\\Component\\Scheduler\\Attribute\\AsCronTask` attribute.
321+
322+
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.
323+
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.
324+
325+
The distinction between these two attributes lies in the options pertaining to the trigger:
326+
327+
#. :class:`Symfony\\Component\\Scheduler\\Attribute\\AsPeriodicTask` attribute:
328+
329+
#. You can configure various options such as ``frequencies``, ``from``, ``until`` and ``jitter``, encompassing options related to the trigger.
330+
331+
#. :class:`Symfony\\Component\\Scheduler\\Attribute\\AsCronTask` attribute:
332+
333+
#. You can configure various options such as ``expression``, ``jitter``, encompassing options related to the trigger.
334+
335+
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.
336+
337+
Managing Scheduled Messages
338+
---------------------------
339+
340+
Modifying Scheduled Messages in real time
341+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
342+
343+
While planning a schedule in advance is beneficial, it is rare for a schedule to remain static over time.
344+
After a certain period, some RecurringMessages may become obsolete, while others may need to be integrated into our planning.
345+
346+
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.
347+
However, this approach can have a flip side.
348+
349+
In the context of our sales company, certain promotions may occur during specific periods and need to be communicated repetitively throughout a given timeframe
350+
or the deletion of old reports needs to be halted under certain circumstances.
351+
352+
This is why the Scheduler incorporates a mechanism to dynamically modify the schedule and consider all changes in real-time.
353+
354+
Strategies for adding, removing, and modifying entries within the Schedule
355+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
356+
357+
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,
358+
resulting in the reset and recalculation of the in-memory stack of recurring messages.
359+
360+
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.
361+
362+
However, if the intention is to completely remove a recurring message and its recurrence,
363+
the :class:`Symfony\\Component\\Scheduler\Schedule` offers a :method:`Symfony\\Component\\Scheduler\Schedule::remove` or a :method:`Symfony\\Component\\Scheduler\Schedule::removeById` method.
364+
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.
365+
366+
In your handler, you can check a condition and, if affirmative, access the :class:`Symfony\\Component\\Scheduler\Schedule` and invoke this method::
367+
368+
// src/Scheduler/MyScheduleProvider.php
369+
namespace App\Scheduler;
370+
371+
#[AsSchedule('uptoyou')]
372+
class SaleTaskProvider implements ScheduleProviderInterface
373+
{
374+
public function getSchedule(): Schedule
375+
{
376+
$this->removeOldReports = RecurringMessage::cron(‘3 8 * * 1’, new CleanUpOldSalesReport());
377+
378+
return $this->schedule ??= (new Schedule())
379+
->with(
380+
// ...
381+
$this->removeOldReports;
382+
);
383+
}
384+
385+
// ...
386+
387+
public function removeCleanUpMessage()
388+
{
389+
$this->getSchedule()->getSchedule()->remove($this->removeOldReports);
390+
}
391+
}
392+
393+
// src/Scheduler/Handler/.php
394+
namespace App\Scheduler\Handler;
395+
396+
#[AsMessageHandler]
397+
class CleanUpOldSalesReportHandler
398+
{
399+
public function __invoke(CleanUpOldSalesReport $cleanUpOldSalesReport): void
400+
{
401+
// do what you have to do
402+
403+
if ($isFinished) {
404+
$this->mySchedule->removeCleanUpMessage();
405+
}
406+
}
407+
}
408+
409+
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,
410+
without making decisions about adding or removing a new recurring message.
411+
412+
For instance, if, due to an external event, there is a need to add a recurrent message aimed at deleting reports,
413+
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.
414+
415+
However, the Scheduler also features an event system that is integrated into a Symfony full-stack application by grafting onto Symfony Messenger events.
416+
These events are dispatched through a listener, providing a convenient means to respond.
417+
418+
Managing Scheduled Messages via Events
419+
--------------------------------------
420+
421+
A strategic event handling
422+
~~~~~~~~~~~~~~~~~~~~~~~~~~
423+
424+
The goal is to provide flexibility in deciding when to take action while preserving decoupling.
425+
Three primary event types have been introduced types
426+
427+
#. PRE_RUN_EVENT
428+
429+
#. POST_RUN_EVENT
430+
431+
#. FAILURE_EVENT
432+
433+
Access to the schedule is a crucial feature, allowing effortless addition or removal of message types.
434+
Additionally, it will be possible to access the currently processed message and its message context.
435+
436+
In consideration of our scenario, you can easily listen to the PRE_RUN_EVENT and check if a certain condition is met.
437+
438+
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).
439+
440+
If you had chosen to handle the deletion of the recurring message, you could have easily done so in a listener for this event.
441+
442+
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::
443+
444+
// src/Scheduler/MyScheduleProvider.php
445+
namespace App\Scheduler;
446+
447+
#[AsSchedule('uptoyou')]
448+
class SaleTaskProvider implements ScheduleProviderInterface
449+
{
450+
public function getSchedule(): Schedule
451+
{
452+
$this->removeOldReports = RecurringMessage::cron(‘3 8 * * 1’, new CleanUpOldSalesReport());
453+
454+
return $this->schedule ??= (new Schedule())
455+
->with(
456+
// ...
457+
);
458+
->before(function(PreRunEvent $event) {
459+
$message = $event->getMessage();
460+
$messageContext = $event->getMessageContext();
461+
462+
// can access the schedule
463+
$schedule = $event->getSchedule()->getSchedule();
464+
465+
// can target directly the RecurringMessage being processed
466+
$schedule->removeById($messageContext->id);
467+
468+
//Allow to call the ShouldCancel() and avoid the message to be handled
469+
$event->shouldCancel(true);
470+
}
471+
->after(function(PostRunEvent $event) {
472+
// Do what you want
473+
}
474+
->onFailure(function(FailureEvent $event) {
475+
// Do what you want
476+
}
477+
}
478+
}
479+
274480
Consuming Messages (Running the Worker)
275481
---------------------------------------
276482

@@ -289,11 +495,6 @@ the Messenger component:
289495
.. image:: /_images/components/scheduler/generate_consume.png
290496
:alt: Symfony Scheduler - generate and consume
291497

292-
.. versionadded:: 6.4
293-
294-
Since version 6.4, you can define your messages via a ``callback`` via the
295-
:class:`Symfony\\Component\\Scheduler\\Trigger\\CallbackMessageProvider`.
296-
297498
Debugging the Schedule
298499
----------------------
299500

@@ -323,20 +524,22 @@ recurring messages. You can narrow down the list to a specific schedule:
323524
# use the --all option to also display the terminated recurring messages
324525
$ php bin/console --all
325526
326-
.. versionadded:: 6.4
327-
328-
The ``--date`` and ``--all`` options were introduced in Symfony 6.4.
329-
330527
Efficient management with Symfony Scheduler
331528
-------------------------------------------
332529

333-
If a worker becomes idle, the recurring messages won't be generated (because they
334-
are created on-the-fly by the scheduler transport).
530+
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).
531+
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.
532+
Upon restart, it will recalculate the messages to be generated from that point onward.
533+
534+
To illustrate, consider a recurring message set to be sent every 3 days.
535+
If a worker is restarted on day 2, the message will be sent 3 days from the restart, on day 5.
536+
537+
While this behavior may not necessarily pose a problem, there is a possibility that it may not align with what you are seeking.
335538

336539
That's why the scheduler allows to remember the last execution date of a message
337540
via the ``stateful`` option (and the :doc:`Cache component </components/cache>`).
338-
This way, when it wakes up again, it looks at all the dates and can catch up on
339-
what it missed::
541+
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.::
542+
340543

341544
// src/Scheduler/MyScheduleProvider.php
342545
namespace App\Scheduler;

0 commit comments

Comments
 (0)