Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
13 / 13
CRAP
100.00% covered (success)
100.00%
47 / 47
ProcessStatus
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
13 / 13
25
100.00% covered (success)
100.00%
47 / 47
 __construct
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 setTtl
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 setMaxTry
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 setMaxProcess
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 setAutoKill
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 verify
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
12 / 12
 each
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 count
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 kill
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
3 / 3
 getProcess
100.00% covered (success)
100.00%
1 / 1
5
100.00% covered (success)
100.00%
8 / 8
 ttlFilter
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
8 / 8
 isZombie
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
1 / 1
 runningFilter
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
2 / 2
1<?php
2
3namespace Qmp\Laravel\AsyncProcessFactory;
4
5use DateTime;
6use Illuminate\Database\Eloquent\Collection as EloquentCollection;
7use Illuminate\Support\Facades\Log;
8use Qmp\Laravel\AsyncProcessFactory\Model\ProcessStatus as ProcessStatusModel;
9
10class ProcessStatus
11{
12    /**
13     * The max process lifetime
14     *
15     * @var string
16     */
17    protected $ttl = '15 minutes';
18
19    /**
20     * The max retry times of the process
21     *
22     * @var int
23     */
24    protected $maxTry = 2;
25
26    /**
27     * The max process
28     *
29     * @var int
30     */
31    protected $maxProcess = 10;
32
33    /**
34     * The model that's holds process information
35     *
36     * @var string
37     */
38    protected $model = ProcessStatusModel::class;
39
40    /**
41     * If process is hanging, kill it
42     *
43     * @var boolean
44     */
45    protected $autoKill = true;
46
47    /**
48     * Undocumented variable
49     *
50     * @var [type]
51     */
52    protected $command;
53
54    /**
55     * The authorized filters in proccessVerify method
56     *
57     * @var array
58     */
59    protected $authorizedFilters = ['ttl', 'running'];
60
61    /**
62     * Class constructor
63     *
64     * @param string $command
65     */
66    public function __construct(string $command)
67    {
68        $this->command = $command;
69    }
70
71    /**
72     * Set ttl
73     *
74     * @param string $ttl
75     * @return self
76     */
77    public function setTtl(string $ttl): self
78    {
79        $this->ttl = $ttl;
80        return $this;
81    }
82
83    /**
84     * Set maxTry
85     *
86     * @param int $maxTry
87     * @return self
88     */
89    public function setMaxTry(int $maxTry): self
90    {
91        $this->maxTry = $maxTry;
92        return $this;
93    }
94
95    /**
96     * Set max process
97     *
98     * @param int $maxProcess
99     * @return self
100     */
101    public function setMaxProcess(int $maxProcess): self
102    {
103        $this->maxProcess = $maxProcess;
104        return $this;
105    }
106
107    /**
108     * Set autokill
109     *
110     * @param bool $autoKill
111     * @return self
112     */
113    public function setAutoKill(bool $autoKill): self
114    {
115        $this->autoKill = $autoKill;
116        return $this;
117    }
118
119    /**
120     * Verify all process and reset those who not pass filters
121     *
122     * @param array $filters
123     * @return self
124     */
125    public function verify(array $filters): self
126    {
127        $this->getProcess()->filter(function ($process) use ($filters) {
128            foreach ($filters as $filter) {
129                if (in_array($filter, $this->authorizedFilters)) {
130                    $filter .= 'Filter';
131                    if (!$this->{$filter}($process)) {
132                        return true;
133                    }
134                }
135            }
136        })->each(function ($process) {
137            $process->pid = null;
138            $process->try += 1;
139            $process->save();
140        });
141
142        return $this;
143    }
144
145    /**
146     * Execute callback on each valid process
147     *
148     * @param Callable $callback
149     * @return void
150     */
151    public function each(callable $callback): void
152    {
153        $this->getProcess(false)->each($callback);
154    }
155
156    /**
157     * Count valid process
158     *
159     * @return int
160     */
161    public function count()
162    {
163        return $this->getProcess(false)->count();
164    }
165
166    /**
167     * Kill the given process
168     *
169     * @param $pid
170     * @return boolean
171     */
172    public function kill($pid): bool
173    {
174        if ($this->autoKill) {
175            return posix_kill($pid, SIGKILL);
176        }
177        return false;
178    }
179
180    /**
181     * Get processes base on condition
182     *
183     * @param boolean $null
184     * @return EloquentCollection
185     */
186    protected function getProcess(bool $null = true): EloquentCollection
187    {
188        $models = $this->model::where('command', $this->command)->where('active', 1);
189        $models = $this->maxTry ? $models->where('try', '<=', $this->maxTry) : $models;
190
191        $collection = $null ? $models->whereNotNull('pid')->get() : $models->whereNull('pid')->get();
192
193        if (!$null) {
194            $diff = $this->maxProcess - $this->getProcess()->count();
195            $diff = $diff > -1 ? $diff : 0;
196            return $collection->take($diff);
197        }
198
199        return $collection;
200    }
201
202    /**
203     * Ttl filter
204     * @param ProcessStatusModel $model
205     * @return boolean
206     * @throws \Exception
207     */
208    protected function ttlFilter(ProcessStatusModel $model): bool
209    {
210        $datetime = new datetime($model->updated_at);
211        $datetime->modify('+' . $this->ttl);
212
213        if (new dateTime() >= $datetime) {
214            if (!$this->isZombie($model->pid)) {
215                Log::debug(var_export("KILL $model->pid $model->command $model->id", true));
216                $this->kill($model->pid);
217                return false;
218            }
219        }
220
221        return true;
222    }
223
224    /**
225     * Check if the process is in zombie state
226     *
227     * @param integer $pid
228     * @return boolean
229     */
230    protected function isZombie(int $pid): bool
231    {
232        return exec('ps axo stat,ppid,pid,comm | grep -w defunct | grep -w ' . $pid) ? true : false;
233    }
234
235    /**
236     * Running filter
237     * @param ProcessStatusModel $model
238     * @return boolean
239     */
240    protected function runningFilter(ProcessStatusModel $model): bool
241    {
242        $running = posix_getpgid($model->pid) !== false;
243        return $running && !$this->isZombie($model->pid);
244    }
245}