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

Skip to content

Commit c128105

Browse files
committed
minor #19244 [Scheduler] Proposal - initial structure for Symfony Scheduler documentation (alli83)
This PR was merged into the 6.3 branch. Discussion ---------- [Scheduler] Proposal - initial structure for Symfony Scheduler documentation Proposal: initial structure for Symfony Scheduler documentation This pull request serves as a proposal for the documentation plan of the Scheduler component. The objective is to make it dynamic, focusing on a use case and exploring all the necessary setups and operational details. The goal is to mirror the thought process one would undergo when implementing the component for a use case. second part (6.4) #19292 Commits ------- b50df97 [Scheduler] Proposal - initial structure for Symfony Scheduler documentation
2 parents deeefe4 + b50df97 commit c128105

File tree

4 files changed

+378
-0
lines changed

4 files changed

+378
-0
lines changed
89.3 KB
Loading
Loading
Loading

scheduler.rst

Lines changed: 378 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,378 @@
1+
Scheduler
2+
=========
3+
4+
.. versionadded:: 6.3
5+
6+
The Scheduler component was introduced in Symfony 6.3
7+
8+
Scheduler is a component designed to manage task scheduling within your PHP application - like running a task each night at 3 am, every 2 weeks except for holidays or any schedule you can imagine.
9+
10+
This component proves highly beneficial for tasks such as database cleanup, automated maintenance (e.g., cache clearing), background processing (queue handling, data synchronization), periodic data updates, or even scheduled notifications (emails, alerts), and more.
11+
12+
This document focuses on using the Scheduler component in the context of a full stack Symfony application.
13+
14+
Installation
15+
------------
16+
17+
In applications using :ref:`Symfony Flex <symfony-flex>`, run this command to
18+
install the scheduler component:
19+
20+
.. code-block:: terminal
21+
22+
$ composer require symfony/scheduler
23+
24+
25+
Introduction to the case
26+
------------------------
27+
28+
Embarking on a task is one thing, but often, the need to repeat that task looms large.
29+
While one could resort to issuing commands and scheduling them with cron jobs, this approach involves external tools and additional configuration.
30+
31+
The Scheduler component emerges as a solution, allowing you to retain control, configuration, and maintenance of task scheduling within our PHP application.
32+
33+
At its core, scheduler allows you to create a task (called a message) that is executed by a service and repeated on some schedule.
34+
Does this sound familiar? Think :doc:`Symfony Messenger docs </components/messenger>`.
35+
36+
But while the system of Messenger proves very useful in various scenarios, there are instances where its capabilities
37+
fall short, particularly when dealing with repetitive tasks at regular intervals.
38+
39+
Let's dive into a practical example within the context of a sales company.
40+
41+
Imagine the company's goal is to send diverse sales reports to customers based on the specific reports each customer chooses to receive.
42+
In constructing the schedule for this scenario, the following steps are taken:
43+
44+
#. Iterate over reports stored in the database and create a recurring task for each report, considering its unique properties. This task, however, should not be generated during holiday periods.
45+
46+
#. Furthermore, you encounter another critical task that needs scheduling: the periodic cleanup of outdated files that are no longer relevant.
47+
48+
On the basis of a case study in the context of a full stack Symfony application, let's dive in and explore how you can set up your system.
49+
50+
Symfony Scheduler basics
51+
------------------------
52+
53+
Differences and parallels between Messenger and Scheduler
54+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
55+
56+
The primary goal is to generate and process reports generation while also handling the removal of outdated reports at specified intervals.
57+
58+
As mentioned, this component, even if it's an independent component, it draws its foundation and inspiration from the Messenger component.
59+
60+
On one hand, it adopts well-established concepts from Messenger (such as message, handler, bus, transport, etc.).
61+
For example, the task of creating a report is considered as a message by the Scheduler, that will be directed, and processed by the corresponding handler.::
62+
63+
// src/Scheduler/Message/SendDailySalesReports.php
64+
namespace App\Scheduler\Message;
65+
66+
class SendDailySalesReports
67+
{
68+
public function __construct(private string $id) {}
69+
70+
public function getId(): int
71+
{
72+
return $this->id;
73+
}
74+
}
75+
76+
// src/Scheduler/Handler/SendDailySalesReportsHandler.php
77+
namespace App\Scheduler\Handler;
78+
79+
#[AsMessageHandler]
80+
class SendDailySalesReportsHandler
81+
{
82+
public function __invoke(SendDailySalesReports $message)
83+
{
84+
// ... do some work - Send the report to the relevant individuals. !
85+
}
86+
}
87+
88+
However, unlike Messenger, the messages will not be dispatched in the first instance. Instead, the aim is to create them based on a predefined frequency.
89+
90+
This is where the specific transport in Scheduler, known as the :class:`Symfony\\Component\\Scheduler\\Messenger\\SchedulerTransport`, plays a crucial role.
91+
The transport autonomously generates directly various messages according to the assigned frequencies.
92+
93+
From (Messenger cycle):
94+
95+
.. image:: /_images/components/messenger/basic_cycle.png
96+
:alt: Symfony Messenger basic cycle
97+
98+
To (Scheduler cycle):
99+
100+
.. image:: /_images/components/scheduler/scheduler_cycle.png
101+
:alt: Symfony Scheduler basic cycle
102+
103+
In Scheduler, the concept of a message takes on a very particular characteristic;
104+
it should be recurrent: It's a :class:`Symfony\\Component\\Scheduler\\RecurringMessage`.
105+
106+
Attach Recurring Messages to a Schedule
107+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
108+
109+
In order to generate various messages based on their defined frequencies, configuration is necessary.
110+
The heart of the scheduling process and its configuration resides in a class that must extend the :class:`Symfony\\Component\\Scheduler\\ScheduleProviderInterface`.
111+
112+
The purpose of this provider is to return a schedule through the method :method:`Symfony\\Component\\Scheduler\\ScheduleProviderInterface::getSchedule` containing your different recurringMessages.
113+
114+
The :class:`Symfony\\Component\\Scheduler\\Attribute\\AsSchedule` attribute, which by default references the ``default`` named schedule, allows you to register on a particular schedule::
115+
116+
// src/Scheduler/MyScheduleProvider.php
117+
namespace App\Scheduler;
118+
119+
#[AsSchedule]
120+
class SaleTaskProvider implements ScheduleProviderInterface
121+
{
122+
public function getSchedule(): Schedule
123+
{
124+
// ...
125+
}
126+
}
127+
128+
.. tip::
129+
130+
By default, if not specified, the schedule name will be ``default``.
131+
In Scheduler, the name of the transport is formed as follows: ``scheduler_nameofyourschedule``.
132+
133+
.. tip::
134+
135+
It is a good practice to memoize your schedule to prevent unnecessary reconstruction if the ``getSchedule`` method is checked by another service or internally within Symfony
136+
137+
138+
Scheduling Recurring Messages
139+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
140+
141+
First and foremost, a RecurringMessage is a message that will be associated with a trigger.
142+
143+
The trigger is what allows configuring the recurrence frequency of your message. Several options are available to us:
144+
145+
#. It can be a cron expression trigger:
146+
147+
.. configuration-block::
148+
149+
.. code-block:: php
150+
151+
RecurringMessage::cron(‘* * * * *’, new Message());
152+
153+
.. tip::
154+
155+
`dragonmantank/cron-expression`_ is required to use the cron expression trigger.
156+
157+
Also, `crontab_helper`_ is a good tool if you need help to construct/understand cron expressions
158+
159+
.. versionadded:: 6.4
160+
161+
Since version 6.4, it is now possible to add and define a timezone as a 3rd argument
162+
163+
#. It can be a periodical trigger through various frequency formats (string / integer / DateInterval)
164+
165+
.. configuration-block::
166+
167+
.. code-block:: php
168+
169+
RecurringMessage::every('10 seconds', new Message());
170+
RecurringMessage::every('3 weeks', new Message());
171+
RecurringMessage::every('first Monday of next month', new Message());
172+
173+
$from = new \DateTimeImmutable('13:47', new \DateTimeZone('Europe/Paris'));
174+
$until = '2023-06-12';
175+
RecurringMessage::every('first Monday of next month', new Message(), $from, $until);
176+
177+
#. It can be a custom trigger implementing :class:`Symfony\\Component\\Scheduler\\TriggerInterface`
178+
179+
If you go back to your scenario regarding reports generation based on your customer preferences.
180+
If the basic frequency is set to a daily basis, you will need to implement a custom trigger due to the specific requirement of not generating reports during public holiday periods::
181+
182+
// src/Scheduler/Trigger/NewUserWelcomeEmailHandler.php
183+
namespace App\Scheduler\Trigger;
184+
185+
class ExcludeHolidaysTrigger implements TriggerInterface
186+
{
187+
public function __construct(private TriggerInterface $inner)
188+
{
189+
}
190+
191+
public function __toString(): string
192+
{
193+
return $this->inner.' (except holidays)';
194+
}
195+
196+
public function getNextRunDate(\DateTimeImmutable $run): ?\DateTimeImmutable
197+
{
198+
if (!$nextRun = $this->inner->getNextRunDate($run)) {
199+
return null;
200+
}
201+
202+
while (!$this->isHoliday($nextRun) { // loop until you get the next run date that is not a holiday
203+
$nextRun = $this->inner->getNextRunDate($nextRun);
204+
}
205+
206+
return $nextRun;
207+
}
208+
209+
private function isHoliday(\DateTimeImmutable $timestamp): bool
210+
{
211+
// app specific logic to determine if $timestamp is on a holiday
212+
// returns true if holiday, false otherwise
213+
}
214+
}
215+
216+
Then, you would have to define your RecurringMessage
217+
218+
.. configuration-block::
219+
220+
.. code-block:: php
221+
222+
RecurringMessage::trigger(
223+
new ExcludeHolidaysTrigger( // your custom trigger wrapper
224+
CronExpressionTrigger::fromSpec('@daily'),
225+
),
226+
new SendDailySalesReports(// ...),
227+
);
228+
229+
The RecurringMessages must be attached to a Schedule::
230+
231+
// src/Scheduler/MyScheduleProvider.php
232+
namespace App\Scheduler;
233+
234+
#[AsSchedule('uptoyou')]
235+
class SaleTaskProvider implements ScheduleProviderInterface
236+
{
237+
public function getSchedule(): Schedule
238+
{
239+
return $this->schedule ??= (new Schedule())
240+
->with(
241+
RecurringMessage::trigger(
242+
new ExcludeHolidaysTrigger( // your custom trigger wrapper
243+
CronExpressionTrigger::fromSpec('@daily'),
244+
),
245+
new SendDailySalesReports()),
246+
RecurringMessage::cron(‘3 8 * * 1’, new CleanUpOldSalesReport())
247+
248+
);
249+
}
250+
}
251+
252+
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.
253+
254+
Consuming Messages (Running the Worker)
255+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
256+
257+
After defining and attaching your RecurringMessages to a schedule, you'll need a mechanism to generate and 'consume' the messages according to their defined frequencies.
258+
This can be achieved using the ``messenger:consume command`` since the Scheduler reuses the Messenger worker.
259+
260+
.. code-block:: terminal
261+
262+
php bin/console messenger:consume scheduler_nameofyourschedule
263+
264+
# use -vv if you need details about what's happening
265+
php bin/console messenger:consume scheduler_nameofyourschedule -vv
266+
267+
.. image:: /_images/components/scheduler/generate_consume.png
268+
:alt: Symfony Scheduler - generate and consume
269+
270+
.. versionadded:: 6.4
271+
272+
Since version 6.4, you can define your message(s) via a ``callback``. This is achieved by defining a :class:`Symfony\\Component\\Scheduler\\Trigger\\CallbackMessageProvider`.
273+
274+
275+
Debugging the Schedule
276+
~~~~~~~~~~~~~~~~~~~~~~
277+
278+
The ``debug:scheduler`` command provides a list of schedules along with their recurring messages.
279+
You can narrow down the list to a specific schedule.
280+
281+
.. versionadded:: 6.4
282+
283+
Since version 6.4, you can even specify a date to determine the next run date using the ``--date`` option.
284+
Additionally, you have the option to display terminated recurring messages using the ``--all`` option.
285+
286+
.. code-block:: terminal
287+
288+
$ php bin/console debug:scheduler
289+
290+
Scheduler
291+
=========
292+
293+
default
294+
-------
295+
296+
------------------- ------------------------- ----------------------
297+
Trigger Provider Next Run
298+
------------------- ------------------------- ----------------------
299+
every 2 days App\Messenger\Foo(0:17..) Sun, 03 Dec 2023 ...
300+
15 4 */3 * * App\Messenger\Foo(0:17..) Mon, 18 Dec 2023 ...
301+
-------------------- -------------------------- ---------------------
302+
303+
Efficient management with Symfony Scheduler
304+
-------------------------------------------
305+
306+
However, if your worker becomes idle, since the messages from your schedule are generated on-the-fly by the schedulerTransport,
307+
they won't be generated during this idle period.
308+
309+
While this might not pose a problem in certain situations, consider the impact for your sales company if a report is missed.
310+
311+
In this case, the scheduler has a feature that allows you to remember the last execution date of a message.
312+
So, when it wakes up again, it looks at all the dates and can catch up on what it missed.
313+
314+
This is where the ``stateful`` option comes into play. This option helps you remember where you left off, which is super handy for those moments when the worker is idle and you need to catch up (for more details, see :doc:`cache </components/cache>`)::
315+
316+
// src/Scheduler/MyScheduleProvider.php
317+
namespace App\Scheduler;
318+
319+
#[AsSchedule('uptoyou')]
320+
class SaleTaskProvider implements ScheduleProviderInterface
321+
{
322+
public function getSchedule(): Schedule
323+
{
324+
$this->removeOldReports = RecurringMessage::cron(‘3 8 * * 1’, new CleanUpOldSalesReport());
325+
326+
return $this->schedule ??= (new Schedule())
327+
->with(
328+
// ...
329+
);
330+
->stateful($this->cache)
331+
}
332+
}
333+
334+
To scale your schedules more effectively, you can use multiple workers.
335+
In such cases, a good practice is to add a :doc:`lock </components/lock>`. for some job concurrency optimization. It helps preventing the processing of a task from being duplicated.::
336+
337+
// src/Scheduler/MyScheduleProvider.php
338+
namespace App\Scheduler;
339+
340+
#[AsSchedule('uptoyou')]
341+
class SaleTaskProvider implements ScheduleProviderInterface
342+
{
343+
public function getSchedule(): Schedule
344+
{
345+
$this->removeOldReports = RecurringMessage::cron(‘3 8 * * 1’, new CleanUpOldSalesReport());
346+
347+
return $this->schedule ??= (new Schedule())
348+
->with(
349+
// ...
350+
);
351+
->lock($this->lockFactory->createLock(‘my-lock’)
352+
}
353+
}
354+
355+
.. tip::
356+
357+
The processing time of a message matters.
358+
If it takes a long time, all subsequent message processing may be delayed. So, it's a good practice to anticipate this and plan for frequencies greater than the processing time of a message.
359+
360+
Additionally, for better scaling of your schedules, you have the option to wrap your message in a :class:`Symfony\\Component\\Messenger\\Message\\RedispatchMessage`.
361+
This allows you to specify a transport on which your message will be redispatched before being further redispatched to its corresponding handler::
362+
363+
// src/Scheduler/MyScheduleProvider.php
364+
namespace App\Scheduler;
365+
366+
#[AsSchedule('uptoyou')]
367+
class SaleTaskProvider implements ScheduleProviderInterface
368+
{
369+
public function getSchedule(): Schedule
370+
{
371+
return $this->schedule ??= (new Schedule())
372+
->with(RecurringMessage::every('5 seconds’), new RedispatchMessage(new Message(), ‘async’))
373+
);
374+
}
375+
}
376+
377+
.. _dragonmantank/cron-expression: https://packagist.org/packages/dragonmantank/cron-expression
378+
.. _crontab_helper: https://crontab.guru/

0 commit comments

Comments
 (0)