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

avoutic / web-framework / 19365573154

14 Nov 2025 01:09PM UTC coverage: 73.336% (-4.2%) from 77.486%
19365573154

push

github

avoutic
Add retry logic to TaskRunnerTask

20 of 25 new or added lines in 1 file covered. (80.0%)

1972 of 2689 relevant lines covered (73.34%)

2.79 hits per line

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

32.0
/src/Task/TaskRunnerTask.php
1
<?php
2

3
/*
4
 * This file is part of WebFramework.
5
 *
6
 * (c) Avoutic <avoutic@gmail.com>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11

12
namespace WebFramework\Task;
13

14
use Carbon\Carbon;
15
use WebFramework\Exception\ArgumentParserException;
16

17
class TaskRunnerTask extends ConsoleTask
18
{
19
    private ?string $taskClass = null;
20
    private bool $isContinuous = false;
21
    private int $delayBetweenRunsInSecs = 1;
22
    private ?int $maxRuntimeInSecs = null;
23
    private int $maxAttempts = 1;
24
    private int $backoffInSecs = 1;
25

26
    public function __construct(
10✔
27
        private readonly TaskRunner $taskRunner,
28
    ) {}
10✔
29

30
    public function getCommand(): string
1✔
31
    {
32
        return 'task:run';
1✔
33
    }
34

35
    public function getDescription(): string
×
36
    {
37
        return 'Run a task';
×
38
    }
39

40
    public function getUsage(): string
×
41
    {
42
        return <<<EOF
×
43
        Usage:
×
44
          {$this->getCommand()} [options] <taskClass>
×
45

46
        The task class should be a fully qualified class name and must implement the
47
        Task interface.
48

49
        Examples:
50
          {$this->getCommand()} --continuous --delay 60 --max-runtime 3600 App\\Task\\MyTask
×
51
          {$this->getCommand()} App\\Task\\MyTask
×
52

53
        Options:
54
          --continuous      Run the task continuously
55
          --delay <secs>    The delay between continuous runs in seconds
56
          --max-runtime <secs> The maximum runtime in seconds
57
          --attempts <num>  The maximum number of attempts (default: 1, no retries)
58
          --backoff <secs>  The backoff delay in seconds between retries (default: 1)
59
        EOF;
×
60
    }
61

62
    public function getArguments(): array
×
63
    {
64
        return [
×
65
            new TaskArgument('taskClass', 'The fully qualified class name of the task to run', true, [$this, 'setTaskClass']),
×
66
        ];
×
67
    }
68

69
    public function getOptions(): array
×
70
    {
71
        return [
×
72
            new TaskOption('continuous', null, 'Run the task continuously', false, [$this, 'setContinuous']),
×
73
            new TaskOption('delay', null, 'The delay between continuous runs in seconds', true, [$this, 'setDelayBetweenRuns']),
×
74
            new TaskOption('max-runtime', null, 'The maximum runtime in seconds', true, [$this, 'setMaxRunTime']),
×
NEW
75
            new TaskOption('attempts', null, 'The maximum number of attempts', true, [$this, 'setMaxAttempts']),
×
NEW
76
            new TaskOption('backoff', null, 'The backoff delay in seconds between retries', true, [$this, 'setBackoff']),
×
77
        ];
×
78
    }
79

80
    public function setTaskClass(string $taskClass): void
×
81
    {
82
        $this->taskClass = $taskClass;
×
83
    }
84

85
    /**
86
     * Set the task to run continuously.
87
     */
88
    public function setContinuous(): void
×
89
    {
90
        $this->isContinuous = true;
×
91
    }
92

93
    /**
94
     * Set the delay between continuous runs.
95
     *
96
     * @param string $secs The delay in seconds
97
     */
98
    public function setDelayBetweenRuns(string $secs): void
×
99
    {
100
        if (!is_numeric($secs))
×
101
        {
102
            throw new ArgumentParserException('Delay between runs must be a number');
×
103
        }
104

105
        $this->delayBetweenRunsInSecs = (int) $secs;
×
106
    }
107

108
    /**
109
     * Set the maximum runtime for continuous execution.
110
     *
111
     * @param string $secs The maximum runtime in seconds
112
     */
113
    public function setMaxRunTime(string $secs): void
×
114
    {
115
        if (!is_numeric($secs))
×
116
        {
117
            throw new ArgumentParserException('Max runtime must be a number');
×
118
        }
119

120
        $this->maxRuntimeInSecs = (int) $secs;
×
121
    }
122

123
    /**
124
     * Set the maximum number of attempts.
125
     *
126
     * @param string $num The maximum number of attempts
127
     */
128
    public function setMaxAttempts(string $num): void
3✔
129
    {
130
        if (!is_numeric($num) || (int) $num < 1)
3✔
131
        {
132
            throw new ArgumentParserException('Attempts must be a positive number');
2✔
133
        }
134

135
        $this->maxAttempts = (int) $num;
1✔
136
    }
137

138
    /**
139
     * Set the backoff delay between retries.
140
     *
141
     * @param string $secs The backoff delay in seconds
142
     */
143
    public function setBackoff(string $secs): void
3✔
144
    {
145
        if (!is_numeric($secs) || (int) $secs < 0)
3✔
146
        {
147
            throw new ArgumentParserException('Backoff must be a non-negative number');
2✔
148
        }
149

150
        $this->backoffInSecs = (int) $secs;
1✔
151
    }
152

153
    public function execute(): void
×
154
    {
155
        if ($this->taskClass === null)
×
156
        {
157
            throw new ArgumentParserException('Task class not set');
×
158
        }
159

160
        if (!class_exists($this->taskClass))
×
161
        {
162
            throw new \RuntimeException("Task class '{$this->taskClass}' does not exist");
×
163
        }
164

165
        $task = $this->taskRunner->get($this->taskClass);
×
166
        if (!$task instanceof Task)
×
167
        {
168
            throw new \RuntimeException("Task {$this->taskClass} does not implement Task");
×
169
        }
170

171
        if ($this->isContinuous)
×
172
        {
173
            $start = Carbon::now();
×
174

175
            while (true)
×
176
            {
NEW
177
                $this->executeWithRetry($task);
×
178

179
                if ($this->maxRuntimeInSecs)
×
180
                {
181
                    if ($start->diffInSeconds() > $this->maxRuntimeInSecs)
×
182
                    {
183
                        break;
×
184
                    }
185
                }
186

187
                Carbon::sleep($this->delayBetweenRunsInSecs);
×
188
            }
189
        }
190
        else
191
        {
NEW
192
            $this->executeWithRetry($task);
×
193
        }
194
    }
195

196
    /**
197
     * Execute a task with retry logic.
198
     *
199
     * @param Task $task The task to execute
200
     *
201
     * @throws \Throwable If the task fails after all retry attempts
202
     */
203
    private function executeWithRetry(Task $task): void
3✔
204
    {
205
        $lastException = null;
3✔
206

207
        for ($attempt = 1; $attempt <= $this->maxAttempts; $attempt++)
3✔
208
        {
209
            try
210
            {
211
                $task->execute();
3✔
212

213
                return;
2✔
214
            }
215
            catch (\Throwable $e)
2✔
216
            {
217
                $lastException = $e;
2✔
218

219
                if ($attempt < $this->maxAttempts)
2✔
220
                {
221
                    $backoffDelay = $this->backoffInSecs * $attempt;
2✔
222
                    Carbon::sleep($backoffDelay);
2✔
223
                }
224
            }
225
        }
226

227
        if ($lastException === null)
1✔
228
        {
NEW
229
            throw new \RuntimeException('Task execution failed but no exception was captured');
×
230
        }
231

232
        throw $lastException;
1✔
233
    }
234
}
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