• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

daycry / cronjob / 15775568161

20 Jun 2025 09:13AM UTC coverage: 69.514% (+4.1%) from 65.424%
15775568161

push

github

daycry
- New Features

130 of 163 new or added lines in 9 files covered. (79.75%)

6 existing lines in 3 files now uncovered.

415 of 597 relevant lines covered (69.51%)

3.21 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.92
/src/JobRunner.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Daycry\CronJob;
6

7
use CodeIgniter\CLI\CLI;
8
use CodeIgniter\I18n\Time;
9
use Config\Services;
10
use DateTime;
11
use Daycry\CronJob\Config\CronJob as BaseConfig;
12
use Daycry\CronJob\Exceptions\TaskAlreadyRunningException;
13
use Exception;
14
use Throwable;
15

16
/**
17
 * Class JobRunner
18
 *
19
 * Handles the execution and management of scheduled jobs.
20
 */
21
class JobRunner
22
{
23
    protected Scheduler $scheduler;
24
    protected ?Time $testTime = null;
25
    protected BaseConfig $config;
26

27
    /**
28
     * @var list<Job>
29
     */
30
    protected array $jobs = [];
31

32
    /**
33
     * @var list<string>
34
     */
35
    protected array $only = [];
36

37
    /**
38
     * JobRunner constructor.
39
     */
40
    public function __construct(?BaseConfig $config = null)
41
    {
42
        $this->config    = $config ?: config('CronJob');
8✔
43
        $this->scheduler = service('scheduler');
8✔
44
    }
45

46
    /**
47
     * Hook: called before a job is executed.
48
     * Override or extend for custom behavior.
49
     */
50
    protected function beforeJob(Job $job): void
51
    {
52
        // Custom logic or event dispatch (e.g., Events::trigger('cronjob.beforeJob', $job));
53
    }
5✔
54

55
    /**
56
     * Hook: called after a job is executed.
57
     * Override or extend for custom behavior.
58
     *
59
     * @param mixed $result
60
     */
61
    protected function afterJob(Job $job, $result, ?Throwable $error): void
62
    {
63
        // Custom logic or event dispatch (e.g., Events::trigger('cronjob.afterJob', $job, $result, $error));
64
    }
5✔
65

66
    /**
67
     * Mide y retorna el tiempo de ejecución de un job (en segundos).
68
     *
69
     * @return array [result, duration]
70
     */
71
    protected function measureJobExecution(callable $callback): array
72
    {
73
        $start    = microtime(true);
6✔
74
        $result   = $callback();
6✔
75
        $duration = microtime(true) - $start;
6✔
76

77
        return [$result, $duration];
6✔
78
    }
79

80
    /**
81
     * Runs all scheduled jobs, respecting dependencies, retries, and hooks.
82
     * Usa topological sort y mide tiempos de ejecución.
83
     */
84
    public function run(): void
85
    {
86
        $this->jobs = [];
8✔
87
        $order      = $this->scheduler->getExecutionOrder();
8✔
88
        $executed   = [];
8✔
89
        $metrics    = [];
8✔
90

91
        foreach ($order as $task) {
8✔
92
            if ($this->shouldSkipTask($task)) {
7✔
93
                continue;
3✔
94
            }
95
            $this->jobs[] = $task;
6✔
96
            $result       = null;
6✔
97
            $error        = null;
6✔
98
            $retries      = $task->getMaxRetries() ?? 0;
6✔
99
            $attempt      = 0;
6✔
100

101
            do {
102
                $attempt++;
6✔
103
                $this->beforeJob($task);
6✔
104
                try {
105
                    [$result, $duration] = $this->measureJobExecution(fn () => $this->processTask($task));
6✔
106
                    $metrics[$task->getName()][] = $duration;
6✔
107
                    $error = null;
6✔
108
                } catch (Throwable $e) {
1✔
109
                    $error = $e;
1✔
110
                    if ($attempt > $retries) {
1✔
NEW
111
                        $this->handleTaskError($task, $e);
×
112
                    } else {
113
                        continue; // Reintenta sin propagar la excepción
1✔
114
                    }
115
                }
116
                $this->afterJob($task, $result, $error);
6✔
117
            } while ($error && $attempt <= $retries);
6✔
118
            $executed[] = $task->getName();
6✔
119
        }
120
        $this->reportMetrics($metrics);
8✔
121
    }
122

123
    /**
124
     * Reporta métricas de ejecución de jobs (puedes personalizar para logs, alertas, etc).
125
     */
126
    protected function reportMetrics(array $metrics): void
127
    {
128
        foreach ($metrics as $job => $runs) {
8✔
129
            $avg = array_sum($runs) / count($runs);
6✔
130
            $this->cliWrite("[METRIC] Job '{$job}' average duration: " . number_format($avg, 4) . 's', 'yellow');
6✔
131
        }
132
    }
133

134
    /**
135
     * Determines if a task should be skipped.
136
     *
137
     * @param Job $task
138
     */
139
    protected function shouldSkipTask($task): bool
140
    {
141
        return (! empty($this->only) && ! in_array($task->getName(), $this->only, true))
7✔
142
               || (! $task->shouldRun($this->testTime) && empty($this->only));
7✔
143
    }
144

145
    /**
146
     * Processes a single task and returns the result.
147
     *
148
     * @param Job $task
149
     *
150
     * @return mixed
151
     */
152
    protected function processTask($task)
153
    {
154
        $error  = null;
6✔
155
        $start  = Time::now();
6✔
156
        $output = null;
6✔
157

158
        $this->cliWrite('Processing: ' . ($task->getName() ?: 'Task'), 'green');
6✔
159
        $task->startLog();
6✔
160

161
        try {
162
            $this->validateTask($task);
6✔
163
            $output = $task->run() ?: \ob_get_contents();
6✔
164
            $this->cliWrite('Executed: ' . ($task->getName() ?: 'Task'), 'cyan');
6✔
165
        } catch (Throwable $e) {
1✔
166
            $this->handleTaskError($task, $e);
1✔
167
            $error = $e;
1✔
168

169
            throw $e;
1✔
170
        } finally {
171
            $this->finalizeTask($task, $start, $output, $error);
6✔
172
        }
173

174
        return $output;
6✔
175
    }
176

177
    /**
178
     * Validates a task before execution.
179
     *
180
     * @param Job $task
181
     *
182
     * @throws Exception|TaskAlreadyRunningException
183
     */
184
    protected function validateTask($task): void
185
    {
186
        if (! $task->saveRunningFlag(true) && $task->getRunType() === 'single') {
6✔
187
            throw new TaskAlreadyRunningException($task);
×
188
        }
189
        if (! $task->status()) {
6✔
UNCOV
190
            throw new Exception(($task->getName() ?: 'Task') . ' is disabled.', 100);
×
191
        }
192
    }
193

194
    /**
195
     * Handles errors during task execution.
196
     *
197
     * @param Job $task
198
     */
199
    protected function handleTaskError($task, Throwable $e): void
200
    {
201
        $this->cliWrite('Failed: ' . ($task->getName() ?: 'Task'), 'red');
1✔
202
        log_message('error', $e->getMessage(), $e->getTrace());
1✔
203
    }
204

205
    /**
206
     * Finalizes a task after execution.
207
     *
208
     * @param Job $task
209
     */
210
    protected function finalizeTask($task, Time $start, ?string $output, ?Throwable $error): void
211
    {
212
        if ($task->shouldRunInBackground()) {
6✔
213
            return;
×
214
        }
215
        if (! $error instanceof TaskAlreadyRunningException) {
6✔
216
            $task->saveRunningFlag(false);
6✔
217
        }
218
        $task->saveLog($output, $error instanceof \Throwable ? $error->getMessage() : $error);
6✔
219
        $this->sendCronJobFinishesEmailNotification($task, $start, $output, $error);
6✔
220
    }
221

222
    /**
223
     * Sends an email notification when a job finishes.
224
     */
225
    public function sendCronJobFinishesEmailNotification(
226
        Job $task,
227
        Time $startAt,
228
        ?string $output = null,
229
        ?Throwable $error = null,
230
    ): void {
231
        if (! $this->config->notification) {
6✔
232
            return;
6✔
233
        }
NEW
234
        $email  = Services::email();
×
235
        $parser = Services::parser();
×
UNCOV
236
        $email->setMailType('html');
×
237
        $email->setFrom($this->config->from, $this->config->fromName);
×
238
        $email->setTo($this->config->to);
×
239
        $email->setSubject($parser->setData(['job' => $task->getName()])->renderString(lang('CronJob.emailSubject')));
×
240
        $email->setMessage($parser->setData([
×
NEW
241
            'name'     => $task->getName(),
×
242
            'runStart' => $startAt,
×
243
            'duration' => $task->duration(),
×
NEW
244
            'output'   => $output,
×
NEW
245
            'error'    => $error,
×
246
        ])->render('Daycry\CronJob\Views\email_notification'));
×
247
        $email->send();
×
248
    }
249

250
    /**
251
     * Restrict execution to only the specified jobs.
252
     *
253
     * @param list<string> $jobs
254
     *
255
     * @return $this
256
     */
257
    public function only(array $jobs = []): self
258
    {
259
        $this->only = $jobs;
1✔
260

261
        return $this;
1✔
262
    }
263

264
    /**
265
     * Get the list of jobs executed in this run.
266
     *
267
     * @return list<Job>
268
     */
269
    public function getJobs(): array
270
    {
271
        return $this->jobs;
3✔
272
    }
273

274
    /**
275
     * Set a test time for job execution (for testing purposes).
276
     *
277
     * @return $this
278
     */
279
    public function withTestTime(string $time): self
280
    {
281
        $this->testTime = Time::createFromInstance(new DateTime($time));
4✔
282

283
        return $this;
4✔
284
    }
285

286
    /**
287
     * Writes output to the CLI if running in CLI mode.
288
     */
289
    protected function cliWrite(string $text, ?string $foreground = null): void
290
    {
291
        if (defined('ENVIRONMENT') && ENVIRONMENT === 'testing') {
6✔
292
            return;
6✔
293
        }
NEW
294
        if (! is_cli()) {
×
295
            return;
×
296
        }
UNCOV
297
        CLI::write('[' . date('Y-m-d H:i:s') . '] ' . $text, $foreground);
×
298
    }
299
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc