Periodic Tasks
PeriodicTask periodically submits a callback to an Executor.
With a ThreadPool, the periodic scheduler decides when to submit work while the pool workers execute the callbacks.
#include <atomic>
#include <chrono>
#include <thread>
#include <vix/threadpool/all.hpp>
int main()
{
vix::threadpool::ThreadPool pool(2);
std::atomic<int> ticks{0};
vix::threadpool::PeriodicTaskConfig config;
config.interval = std::chrono::milliseconds{100};
config.run_immediately = true;
auto task = pool.schedule_every(
[&ticks](){
ticks.fetch_add(1, std::memory_order_relaxed);
},
config
);
if (!task.start())
{
return 1;
}
std::this_thread::sleep_for(
std::chrono::milliseconds{350}
);
task.stop();
task.join();
pool.wait_idle();
return ticks.load(std::memory_order_relaxed) > 0 ? 0 : 1;
}A periodic task is not started automatically.
Execution model
PeriodicTask separates scheduling from callback execution.
PeriodicTask scheduler thread
↓
tick
↓
Executor::post()
↓
executor runtime
↓
callbackWith a ThreadPool:
PeriodicTask
scheduler thread
↓
ThreadPool::post()
↓
Scheduler
↓
Worker
↓
callbackThe periodic scheduler thread does not normally execute the callback itself when using ThreadPool.
It only submits it.
One scheduler thread per PeriodicTask
Each running PeriodicTask owns one lightweight std::thread responsible for timing.
For example:
PeriodicTask A
└── scheduler thread A
PeriodicTask B
└── scheduler thread BThese scheduler threads are separate from ThreadPool worker threads.
Creating many periodic tasks therefore also creates many scheduler threads.
Create a PeriodicTask directly
A periodic task can be created from any Executor:
vix::threadpool::PeriodicTask task(
pool,
[](){
perform_work();
}
);The constructor accepts:
Executor&
Callback
PeriodicTaskConfigThe callback type is:
using Callback = std::function<void()>;The task remains stopped until start() is called.
Create one through ThreadPool
ThreadPool provides:
pool.schedule_every(callback, config);For example:
auto task = pool.schedule_every(
[](){
perform_work();
}
);This creates a PeriodicTask bound to the pool.
It does not start it.
You must still call:
task.start();schedule_every() does not schedule immediately
Despite its name:
auto task = pool.schedule_every(
[](){
perform_work();
}
);only constructs the periodic scheduling object.
At this point:
task.running() == falseNo scheduler thread has been started and no callback has been submitted.
Execution begins only after:
task.start();PeriodicTaskConfig
Periodic behavior is configured through:
vix::threadpool::PeriodicTaskConfigThe configuration contains:
std::chrono::milliseconds interval;
vix::threadpool::TaskOptions options;
bool run_immediately;
bool stop_on_post_failure;The defaults are:
interval 1000 ms
run_immediately false
stop_on_post_failure true
options default TaskOptionsConfigure the interval
Set the interval directly:
vix::threadpool::PeriodicTaskConfig config;
config.interval = std::chrono::milliseconds{250};or use:
auto config = vix::threadpool::PeriodicTaskConfig::every(
std::chrono::milliseconds{250}
);Both represent a 250 millisecond periodic interval.
Interval normalization
Periodic intervals must be positive.
The helper:
vix::threadpool::PeriodicTaskConfig::normalize_interval(
value
);converts non-positive values to:
1 msFor example:
const auto interval =
vix::threadpool::PeriodicTaskConfig::normalize_interval(
std::chrono::milliseconds{0}
);produces:
1 msThe same applies to negative values.
every() normalizes immediately
This:
auto config = vix::threadpool::PeriodicTaskConfig::every(
std::chrono::milliseconds{0}
);produces:
config.interval = 1 msevery() never returns a non-positive interval.
Direct configuration is normalized on task construction
This is also safe:
vix::threadpool::PeriodicTaskConfig config;
config.interval = std::chrono::milliseconds{0};
vix::threadpool::PeriodicTask task(
pool,
[](){},
config
);The PeriodicTask constructor stores:
config.normalized()so the effective interval becomes:
1 msInspect the stored normalized configuration with:
task.config();First execution
By default:
config.run_immediately = false;The scheduler waits one full interval before the first submission.
Conceptually:
start()
↓
wait interval
↓
submit tick 1
↓
wait until next tick
↓
submit tick 2For a 500 millisecond interval:
start
|
| 500 ms
v
tick 1
|
| 500 ms
v
tick 2Run immediately
Set:
config.run_immediately = true;to submit one callback as soon as the scheduler thread begins running.
The sequence becomes:
start()
↓
submit immediate tick
↓
wait interval
↓
submit next tickFor example:
vix::threadpool::PeriodicTaskConfig config;
config.interval = std::chrono::seconds{1};
config.run_immediately = true;requests one immediate submission followed by periodic submissions.
Start the task
Use:
const bool started = task.start();A successful call:
creates scheduler thread
sets running state
returns trueCheck it:
if (!task.start())
{
return 1;
}Conditions required by start()
start() returns false when the periodic task has no valid executor or no callback.
For example, an empty task:
vix::threadpool::PeriodicTask task;cannot start:
task.start();returns:
falseIts state remains:
running() false
submitted_ticks() 0
failed_posts() 0Starting an already running task
Calling start() again while the task is already running returns:
falseFor example:
if (!task.start())
{
return 1;
}
const bool secondStart = task.start();gives:
secondStart = falseThe existing scheduler thread continues running.
A second thread is not created.
Check running state
Use:
if (task.running())
{
// Periodic submission is active.
}running() describes the scheduler loop.
It does not mean a callback is currently executing.
For example:
PeriodicTask running = true
scheduler sleeping until next tick
callback count = 0is a valid state.
Check scheduler-thread ownership
Use:
task.joinable();to determine whether the internal scheduler thread is currently joinable.
This is different from:
task.running();A scheduler thread can have finished while its std::thread object remains joinable until join() is called.
Stop periodic submission
Use:
task.stop();stop() changes the running flag to false.
It is safe to call repeatedly:
task.stop();
task.stop();
task.stop();The operation is idempotent.
stop() does not join
This:
task.stop();requests the scheduler loop to finish.
It does not wait for the scheduler thread to terminate.
The normal shutdown sequence is:
task.stop();
task.join();Conceptually:
stop()
↓
running = false
↓
scheduler eventually exits
join()
↓
wait for scheduler threadStop before start
Calling:
task.stop();
task.join();before start() is safe.
No scheduler thread exists, so join() simply returns.
Stopping does not cancel submitted callbacks
This distinction is important.
Suppose:
tick submitted
↓
callback queued in ThreadPool
↓
task.stop()The callback already accepted by the pool remains ordinary ThreadPool work.
stop() prevents future periodic submissions.
It does not remove or cancel callbacks already submitted.
Use the task's TaskOptions cancellation mechanisms when submitted callbacks themselves need cancellation semantics.
Join the scheduler thread
Use:
task.join();after stopping.
If the internal thread is not joinable, the operation returns immediately.
The method is noexcept.
The ordinary lifecycle is:
if (!task.start())
{
return 1;
}
// ...
task.stop();
task.join();stop() does not wake the sleeping scheduler immediately
The current scheduler uses:
std::this_thread::sleep_until(next);to wait for each tick.
stop() only updates an atomic flag.
It does not interrupt that sleep.
Therefore:
task.stop();
task.join();can wait until the current sleep_until() finishes.
For example, with:
interval = 30 secondsstopping just after the scheduler begins sleeping can cause join() to wait close to the remaining interval.
The scheduler checks the running state again after waking and exits without submitting another tick.
Destructor
PeriodicTask automatically performs:
stop();
join();in its destructor.
Therefore:
{
auto task = pool.schedule_every(
[](){
perform_work();
}
);
task.start();
}stops and joins the scheduler thread when the PeriodicTask object leaves scope.
The same sleep behavior applies to destruction.
If the scheduler is sleeping for a long interval, destruction can block until that sleep finishes.
Executor lifetime
PeriodicTask stores an ExecutorRef.
That reference is non-owning.
Conceptually:
PeriodicTask
│
└────► Executor
non-owningThe executor must therefore outlive the periodic task and its scheduler thread.
The natural order is:
vix::threadpool::ThreadPool pool(4);
{
auto task = pool.schedule_every(
[](){
perform_work();
}
);
task.start();
// ...
task.stop();
task.join();
}
pool.shutdown();Do not destroy the pool while a periodic scheduler can still dereference it.
Pool shutdown while periodic scheduling is active
If the pool is shut down while a periodic task is still running:
PeriodicTask
↓
next tick
↓
ThreadPool::post()
↓
pool rejects submissionthe periodic task observes a post failure.
With the default configuration:
config.stop_on_post_failure = true;the periodic scheduler stops itself after that failed submission.
The already accepted callbacks remain governed by the pool's normal shutdown behavior.
Post failure behavior
Each tick calls:
executor.post(
callback,
config.options
);If post() returns true:
submitted_ticks += 1If it returns false:
failed_posts += 1Then stop_on_post_failure determines whether the periodic scheduler continues.
Stop on post failure
The default is:
config.stop_on_post_failure = true;The behavior is:
post tick
↓
post() returns false
↓
failed_posts += 1
↓
running = false
↓
scheduler exitsThis prevents a periodic scheduler from continuing indefinitely when its executor no longer accepts work.
Continue after post failure
Set:
config.stop_on_post_failure = false;to continue periodic attempts after failed submissions.
Conceptually:
tick
↓
post fails
↓
failed_posts += 1
↓
wait until next tick
↓
try againFor example, if the executor remains stopped:
failed_posts
1
2
3
4
...continues increasing until the periodic task itself is stopped.
Invalid executor always stops
If the stored executor reference itself becomes invalid inside submit_tick():
executor invalid
↓
failed_posts += 1
↓
running = falseThis path stops regardless of stop_on_post_failure.
A normally constructed task keeps its ExecutorRef, so the relevant lifetime rule remains that the referenced executor must stay alive.
Submitted ticks
Inspect successful post() calls with:
const std::uint64_t ticks = task.submitted_ticks();This counts:
periodic submissions for which Executor::post()
returned trueIt does not count scheduler wakeups independently.
For example:
5 successful posts
2 failed posts
submitted_ticks() = 5
failed_posts() = 2Submitted does not mean completed
A successful periodic submission means the executor accepted or handled the post.
It does not mean the callback later completed successfully.
With ThreadPool:
PeriodicTask
↓
post() returns true
↓
submitted_ticks += 1
↓
callback waits in queue
↓
callback eventually runsThe callback can later:
complete
throw
be classified as cancelled
be classified as timed outwithout changing the periodic task's submitted_ticks() counter.
Callback failures are not generally post failures
With a ThreadPool, post() normally returns after the task has been accepted into the runtime.
If the callback later throws:
post() returned true
↓
submitted_ticks += 1
↓
worker executes callback
↓
callback throws
↓
ThreadPool records execution failurePeriodicTask::failed_posts() does not increase because submission itself succeeded.
Use ThreadPool metrics when callback execution outcomes matter.
InlineExecutor behaves differently
PeriodicTask can also use InlineExecutor:
vix::threadpool::InlineExecutor executor;
vix::threadpool::PeriodicTask task(
executor,
[](){
perform_work();
}
);InlineExecutor::post() executes the callback synchronously on the thread calling post().
For PeriodicTask, that is the periodic scheduler thread.
Conceptually:
PeriodicTask scheduler thread
↓
InlineExecutor::post()
↓
callback executes immediately
on scheduler threadThis differs significantly from ThreadPool execution.
ThreadPool callbacks are asynchronous from the timer thread
With ThreadPool:
timer thread
↓
post()
↓
returns after acceptance
↓
timer thread continues scheduling
worker
↓
executes callback independentlyThe periodic interval therefore controls submission times, not callback completion times.
Callbacks can overlap
A periodic task does not wait for one ThreadPool callback to finish before submitting the next tick.
Suppose:
interval = 100 ms
callback duration = 500 msThe scheduler can submit:
t = 0 ms callback A
t = 100 ms callback B
t = 200 ms callback C
t = 300 ms callback D
...while earlier callbacks are still running or queued.
Depending on the worker count and task options, multiple invocations of the same periodic callback can therefore execute concurrently.
PeriodicTask is not fixed-delay execution
The ThreadPool behavior is not:
run callback
↓
wait for callback completion
↓
wait interval
↓
run callback againInstead, it is based on periodic submissions:
tick
↓
post callback
interval
↓
tick
↓
post callbackCallback completion is independent from the scheduler timer.
If overlapping executions are not acceptable, the application must provide its own serialization or running-state guard.
Shared callback state must be thread-safe
Because multiple callback instances can overlap:
int counter = 0;
auto task = pool.schedule_every(
[&counter](){
++counter;
},
config
);can contain a data race.
Use appropriate synchronization:
std::atomic<int> counter{0};
auto task = pool.schedule_every(
[&counter](){
counter.fetch_add(
1,
std::memory_order_relaxed
);
},
config
);Normal C++ concurrent-access rules apply.
Fixed-rate scheduling after startup
After the first scheduled time is established, the scheduler advances its target time with:
next += config.interval;rather than recalculating:
now + intervalafter every successful ThreadPool post.
Conceptually:
target 1
↓
target 2 = target 1 + interval
↓
target 3 = target 2 + intervalThis gives the scheduler a fixed-rate target sequence.
Scheduler delay can produce catch-up submissions
Because target times advance by the configured interval, if the scheduler thread itself wakes late:
expected tick 1 100 ms
expected tick 2 200 ms
expected tick 3 300 ms
scheduler resumes at 350 msthen after submitting one tick, the next target can already be in the past.
std::this_thread::sleep_until() returns immediately for a past target.
The scheduler can therefore submit several ticks close together while its target sequence catches up.
PeriodicTask does not intentionally drop missed ticks.
run_immediately establishes the later schedule afterward
When:
config.run_immediately = true;the implementation first calls:
submit immediate tickand then calculates:
next = steady_clock::now() + intervalFor a ThreadPool, the immediate post() usually returns quickly after acceptance.
For a synchronous executor such as InlineExecutor, the immediate callback finishes before the next scheduled time is calculated.
InlineExecutor timing includes callback execution
With InlineExecutor, the periodic scheduler itself runs the callback.
Therefore:
scheduler
↓
callback begins
↓
callback executes
↓
callback returns
↓
scheduler continuesLong callback execution directly delays the scheduling loop.
This is different from ThreadPool, where callback execution occurs independently on workers.
TaskOptions for every tick
PeriodicTaskConfig contains:
config.options;These options are passed to every callback submission.
For example:
vix::threadpool::PeriodicTaskConfig config;
config.interval = std::chrono::milliseconds{100};
config.options.set_priority(
vix::threadpool::TaskPriority::high
);Every periodic callback is posted with high priority.
Priority
Periodic callbacks can use:
config.options.set_priority(
vix::threadpool::TaskPriority::high
);Each accepted tick becomes an ordinary high-priority task in its selected worker queue.
Priority does not alter the periodic clock.
It only affects normal queue ordering after submission.
Worker affinity
Affinity can be used:
config.options.set_affinity(
vix::threadpool::WorkerId{2}
);Every periodic tick then carries the same worker affinity.
Conceptually:
tick 1 ──┐
tick 2 ──┤
tick 3 ──┼──► Worker 2
tick 4 ──┘This can serialize callback execution on one worker, although callbacks can still accumulate in that worker's queue when they are submitted faster than they complete.
See Worker Affinity.
Cancellation
A cancellation token can be attached to every tick:
vix::threadpool::CancellationSource source;
vix::threadpool::PeriodicTaskConfig config;
config.options.set_cancellation(
source.token()
);The token is copied into every post.
Cancelling it does not stop the PeriodicTask scheduler itself.
It affects the submitted callback tasks.
Conceptually:
source.request_cancel()
↓
future periodic post still happens
↓
callback task receives cancelled token
↓
ThreadPool can skip callback executionThe periodic scheduler continues until:
task.stop();or a configured post failure stops it.
Cancellation is different from stopping periodic scheduling
These operations solve different problems.
source.request_cancel();means:
submitted callback work should observe cancellationwhile:
task.stop();means:
do not schedule future periodic submissionsUse both when both behaviors are required.
Deadline reuse
A Deadline inside:
config.optionsis copied to every tick.
A deadline is absolute.
For example:
config.options.set_deadline(
vix::threadpool::Deadline::after(
std::chrono::seconds{5}
)
);creates one absolute time point when the config is built.
All later periodic submissions receive that same deadline.
After it expires, later callback tasks can be skipped.
The deadline is not automatically renewed for each periodic tick.
Per-tick deadlines require application logic
If every tick should receive a fresh deadline relative to its own submission time, one static:
config.options.deadlinecannot express that.
PeriodicTask reuses the configured TaskOptions for each submission.
It does not regenerate:
Deadline::after(...)at every tick.
Use callback-level logic or a different scheduling composition when a fresh absolute deadline is required for each execution.
Timeout
A timeout in periodic options:
config.options.set_timeout(
vix::threadpool::Timeout::milliseconds(100)
);is copied to every tick.
Each submitted callback task receives its own execution-duration timeout observation.
The timeout does not control the scheduler interval and does not stop a callback after 100 milliseconds.
See Timeouts.
Queue pressure
Because periodic submission does not wait for callback completion, slow work can accumulate.
Suppose:
interval 10 ms
callback duration 500 msThe scheduler can submit work much faster than workers complete it.
Conceptually:
scheduler:
tick tick tick tick tick tick ...
workers:
callback
callback
queues:
callback
callback
callback
callback
...With unbounded queues, pending work can continue growing according to available memory.
With bounded queues, later periodic posts can fail.
Bounded queue rejection
Suppose the pool uses:
vix::threadpool::ThreadPoolConfig poolConfig;
poolConfig.thread_count = 2;
poolConfig.max_queue_size = 4;
vix::threadpool::ThreadPool pool(poolConfig);If periodic callbacks accumulate until the selected worker queue cannot accept another task:
Executor::post() returns falseThen:
failed_posts += 1With the default:
config.stop_on_post_failure = true;the periodic scheduler stops.
See Queue and Rejection Policies.
Observe counters
The periodic scheduler exposes two counters:
task.submitted_ticks();
task.failed_posts();For example:
const auto submitted = task.submitted_ticks();
const auto failed = task.failed_posts();Both are atomic counters and can be inspected while the scheduler is running.
They describe submission behavior, not full callback execution outcomes.
Counters are cumulative
Stopping a periodic task does not reset:
submitted_ticks
failed_postsIf the same object is started again after a complete stop and join cycle, the counters continue from their previous values.
Conceptually:
first run
submitted_ticks = 5
stop()
join()
second run
2 more successful posts
submitted_ticks = 7There is no public counter reset operation.
Restarting a PeriodicTask
A stopped task can be started again after its previous scheduler thread has been joined:
task.stop();
task.join();
if (!task.start())
{
return 1;
}The callback, executor reference, configuration, and counters remain stored.
Join before restarting
Do not restart a periodic task while its previous std::thread remains joinable.
Use:
task.stop();
task.join();
task.start();rather than:
task.stop();
task.start();The current implementation assigns a new std::thread inside start(). The previous thread must therefore have been joined first.
The same rule applies when the scheduler stopped itself after a post failure.
Call join() before starting it again.
Non-copyable
PeriodicTask cannot be copied.
copy construction disabled
copy assignment disabledThis reflects ownership of its scheduler thread and state.
Movable
PeriodicTask provides move construction and move assignment.
However, the scheduler thread executes a loop tied to the object's internal state.
For safe application use, move periodic task objects while they are stopped and joined.
The normal safe sequence is:
stop
↓
join
↓
move objectAvoid relocating a running periodic task.
schedule_every() return value
This pattern is the natural ThreadPool API:
auto task = pool.schedule_every(
[](){
perform_work();
},
config
);The returned object begins stopped and can be owned directly by the caller without managing the lower-level ExecutorRef.
Complete lifecycle
A complete periodic task lifecycle is:
construct
↓
not running
↓
start()
↓
scheduler thread created
↓
periodic post attempts
↓
stop()
↓
running = false
↓
join()
↓
scheduler thread reclaimed
↓
optional restart
or
destructionIn code:
auto task = pool.schedule_every(
[](){
perform_work();
},
vix::threadpool::PeriodicTaskConfig::every(
std::chrono::milliseconds{250}
)
);
if (!task.start())
{
return 1;
}
// Application work.
task.stop();
task.join();Complete observed-ticks example
#include <atomic>
#include <chrono>
#include <thread>
#include <vix/threadpool/all.hpp>
int main()
{
vix::threadpool::ThreadPool pool(2);
std::atomic<int> observed{0};
vix::threadpool::PeriodicTaskConfig config;
config.interval = std::chrono::milliseconds{50};
config.run_immediately = true;
auto task = pool.schedule_every(
[&observed](){
observed.fetch_add(
1,
std::memory_order_relaxed
);
},
config
);
if (!task.start())
{
return 1;
}
std::this_thread::sleep_for(
std::chrono::milliseconds{180}
);
task.stop();
task.join();
pool.wait_idle();
if (task.failed_posts() != 0)
{
return 1;
}
return observed.load(std::memory_order_relaxed) > 0 ? 0 : 1;
}pool.wait_idle() after task.join() waits for callbacks that were already submitted before periodic scheduling stopped.
PeriodicTask vs manual loop
A manual loop might look like:
while (running)
{
pool.post([](){
perform_work();
});
std::this_thread::sleep_for(
std::chrono::seconds{1}
);
}PeriodicTask packages the recurring scheduling state into one abstraction:
interval
first-run behavior
task options
post-failure behavior
scheduler thread
submission counters
lifecycleThe callback itself remains an ordinary executor task.
PeriodicTask does not guarantee exact real-time execution
The configured interval is a scheduling target.
Actual callback start time depends on:
operating-system scheduler
PeriodicTask timer-thread wakeup
ThreadPool scheduling
worker availability
queue length
priority
affinity
other tasksTherefore:
interval = 100 msdoes not guarantee that user callback code begins exactly every 100 milliseconds.
PeriodicTask is a periodic task-submission abstraction, not a hard real-time scheduler.
Choosing an interval
A shorter interval creates more frequent post attempts.
The useful interval should account for:
callback execution cost
available workers
queue capacity
acceptable overlap
executor load
required scheduling precisionIf periodic submissions arrive faster than the executor can process them, backlog or rejection can result.
The interval should therefore be part of workload design rather than only timer configuration.
Periodic model summary
The normal ThreadPool path is:
PeriodicTask::start()
↓
scheduler thread
↓
run_immediately?
┌────┴────┐
yes no
│ │
post tick │
└────┬────┘
↓
next = now + interval
↓
sleep_until(next)
↓
still running?
┌────┴────┐
no yes
│ │
exit ▼
Executor::post()
↓
accepted?
┌───┴───┐
yes no
│ │
submitted++ failed++
│ │
│ stop_on_failure?
│ ┌─┴─┐
│ yes no
│ │ │
│ stop │
└──────┴───┘
↓
next += interval
↓
repeatThe important properties are:
PeriodicTaskperiodically callsExecutor::post().- Each running periodic task owns one scheduler thread.
- The scheduler thread is separate from ThreadPool workers.
ThreadPool::schedule_every()constructs a periodic task but does not start it.start()must be called explicitly.- The default interval is 1000 milliseconds.
- Non-positive intervals normalize to 1 millisecond.
run_immediatelydefaults tofalse.- With
run_immediately == false, the first tick occurs after one interval. - With
run_immediately == true, one tick is submitted before the first interval wait. stop()requests scheduler termination but does not join.join()should normally followstop().stop()does not interrupt the scheduler's currentsleep_until(), so joining or destruction can wait for the remaining interval.- The destructor calls
stop()andjoin(). - The executor reference is non-owning and must remain alive.
- Stopping periodic scheduling does not cancel callbacks already submitted.
- With ThreadPool, callback execution is asynchronous from the timer thread.
- Periodic callbacks can overlap when execution takes longer than the interval.
- Slow callbacks can create queue backlog.
- Scheduling uses fixed-rate target advancement with
next += interval. - Delayed scheduler wakeups can produce closely spaced catch-up submissions.
TaskOptionsare reused for every periodic submission.- Priority, affinity, cancellation, deadline, timeout, queue capacity, and rejection keep their normal executor semantics.
- An absolute deadline stored in the configuration is not renewed for each tick.
- Cancellation of callback tasks does not stop the periodic scheduler.
submitted_ticks()counts successfulExecutor::post()calls.failed_posts()countsExecutor::post()calls that returned false.- Successful submission does not mean successful callback completion.
- The default
stop_on_post_failureistrue. stop_on_post_failure == falseallows repeated post attempts after failure.- Counters remain cumulative across stop and restart cycles.
- Join the previous scheduler thread before restarting.
PeriodicTaskis non-copyable.- For safe use, move it only while stopped and joined.
- The interval is a scheduling target, not a hard real-time execution guarantee.
Continue with Metrics and Statistics for observing executor activity, task outcomes, queue pressure, and timing.