Completed
Pull Request — master (#31)
by Matthew
06:07
created

JobManager   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 498
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 88.78%

Importance

Changes 0
Metric Value
wmc 55
lcom 1
cbo 10
dl 0
loc 498
ccs 261
cts 294
cp 0.8878
rs 6.8
c 0
b 0
f 0

19 Methods

Rating   Name   Duplication   Size   Complexity  
A pruneErroneousJobs() 0 13 1
A updateExpired() 0 17 1
B countJobsByStatus() 0 30 4
B resetSaveOk() 0 23 4
A addWorkerNameCriterion() 0 10 3
A pruneArchivedJobs() 0 8 1
B updateNearestBatch() 0 26 2
A countLiveJobs() 0 11 1
B getJobCount() 0 45 4
A getStatus() 0 20 4
A getStatusByEntityName() 0 22 3
B getJob() 0 21 5
A getJobQueryBuilder() 0 17 2
A addStandardPredicate() 0 17 1
B takeJob() 0 30 3
B updateBatchJob() 0 29 5
A getWorkersAndMethods() 0 20 3
A archiveAllJobs() 0 17 2
B runArchive() 0 29 6

How to fix   Complexity   

Complex Class

Complex classes like JobManager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use JobManager, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Dtc\QueueBundle\ORM;
4
5
use Doctrine\ORM\EntityManager;
6
use Doctrine\ORM\EntityRepository;
7
use Doctrine\ORM\QueryBuilder;
8
use Dtc\QueueBundle\Doctrine\BaseJobManager;
9
use Dtc\QueueBundle\Entity\Job;
10
use Dtc\QueueBundle\Model\BaseJob;
11
use Dtc\QueueBundle\Model\RetryableJob;
12
use Symfony\Component\Process\Exception\LogicException;
13
14
class JobManager extends BaseJobManager
15
{
16
    use CommonTrait;
17
    protected static $saveInsertCalled = null;
18
    protected static $resetInsertCalled = null;
19
20 3
    public function countJobsByStatus($objectName, $status, $workerName = null, $method = null)
21
    {
22
        /** @var EntityManager $objectManager */
23 3
        $objectManager = $this->getObjectManager();
24
25
        $queryBuilder = $objectManager
26 3
            ->createQueryBuilder()
27 3
            ->select('count(a.id)')
28 3
            ->from($objectName, 'a')
29 3
            ->where('a.status = :status');
30
31 3
        if (null !== $workerName) {
32 1
            $queryBuilder->andWhere('a.workerName = :workerName')
33 1
                ->setParameter(':workerName', $workerName);
34 1
        }
35
36 3
        if (null !== $method) {
37 1
            $queryBuilder->andWhere('a.method = :method')
38 1
                ->setParameter(':method', $workerName);
39 1
        }
40
41 3
        $count = $queryBuilder->setParameter(':status', $status)
42 3
            ->getQuery()->getSingleScalarResult();
43
44 3
        if (!$count) {
45 1
            return 0;
46
        }
47
48 3
        return $count;
49
    }
50
51
    /**
52
     * @param string|null $workerName
53
     * @param string|null $method
54
     *
55
     * @return int Count of jobs pruned
56
     */
57 1
    public function pruneErroneousJobs($workerName = null, $method = null)
58
    {
59
        /** @var EntityManager $objectManager */
60 1
        $objectManager = $this->getObjectManager();
61 1
        $queryBuilder = $objectManager->createQueryBuilder()->delete($this->getJobArchiveClass(), 'j');
62 1
        $queryBuilder->where('j.status = :status')
63 1
            ->setParameter(':status', BaseJob::STATUS_ERROR);
64
65 1
        $this->addWorkerNameCriterion($queryBuilder, $workerName, $method);
66 1
        $query = $queryBuilder->getQuery();
67
68 1
        return intval($query->execute());
69
    }
70
71 19
    protected function resetSaveOk($function)
72 1
    {
73 19
        $objectManager = $this->getObjectManager();
74 19
        $splObjectHash = spl_object_hash($objectManager);
75
76 19
        if ('save' === $function) {
77
            $compare = static::$resetInsertCalled;
78
        } else {
79 19
            $compare = static::$saveInsertCalled;
80
        }
81
82 19
        if ($splObjectHash === $compare) {
83
            // Insert SQL is cached...
84
            $msg = "Can't call save and reset within the same process cycle (or using the same EntityManager)";
85
            throw new LogicException($msg);
86
        }
87
88 19
        if ('save' === $function) {
89
            static::$saveInsertCalled = spl_object_hash($objectManager);
90
        } else {
91 19
            static::$resetInsertCalled = spl_object_hash($objectManager);
92
        }
93 19
    }
94
95
    /**
96
     * @param string $workerName
97
     * @param string $method
98
     */
99 13
    protected function addWorkerNameCriterion(QueryBuilder $queryBuilder, $workerName = null, $method = null)
100
    {
101 13
        if (null !== $workerName) {
102 5
            $queryBuilder->andWhere('j.workerName = :workerName')->setParameter(':workerName', $workerName);
103 5
        }
104
105 13
        if (null !== $method) {
106 4
            $queryBuilder->andWhere('j.method = :method')->setParameter(':method', $method);
107 4
        }
108 13
    }
109
110 1
    protected function updateExpired($workerName = null, $method = null)
111
    {
112
        /** @var EntityManager $objectManager */
113 1
        $objectManager = $this->getObjectManager();
114 1
        $queryBuilder = $objectManager->createQueryBuilder()->update($this->getJobClass(), 'j');
115 1
        $queryBuilder->set('j.status', ':newStatus');
116 1
        $queryBuilder->where('j.expiresAt <= :expiresAt')
117 1
            ->setParameter(':expiresAt', new \DateTime());
118 1
        $queryBuilder->andWhere('j.status = :status')
119 1
            ->setParameter(':status', BaseJob::STATUS_NEW)
120 1
            ->setParameter(':newStatus', Job::STATUS_EXPIRED);
121
122 1
        $this->addWorkerNameCriterion($queryBuilder, $workerName, $method);
123 1
        $query = $queryBuilder->getQuery();
124
125 1
        return intval($query->execute());
126
    }
127
128
    /**
129
     * Removes archived jobs older than $olderThan.
130
     *
131
     * @param \DateTime $olderThan
132
     */
133 1
    public function pruneArchivedJobs(\DateTime $olderThan)
134
    {
135 1
        return $this->removeOlderThan(
136 1
            $this->getJobArchiveClass(),
137 1
                'updatedAt',
138
                $olderThan
139 1
        );
140
    }
141
142 2
    public function getJobCount($workerName = null, $method = null)
143
    {
144
        /** @var EntityManager $objectManager */
145 2
        $objectManager = $this->getObjectManager();
146 2
        $queryBuilder = $objectManager->createQueryBuilder();
147
148 2
        $queryBuilder = $queryBuilder->select('count(j)')->from($this->getJobClass(), 'j');
149
150 2
        $where = 'where';
151 2
        if (null !== $workerName) {
152
            if (null !== $method) {
153
                $queryBuilder->where($queryBuilder->expr()->andX(
154
                    $queryBuilder->expr()->eq('j.workerName', ':workerName'),
155
                                                $queryBuilder->expr()->eq('j.method', ':method')
156
                ))
157
                    ->setParameter(':method', $method);
158
            } else {
159
                $queryBuilder->where('j.workerName = :workerName');
160
            }
161
            $queryBuilder->setParameter(':workerName', $workerName);
162
            $where = 'andWhere';
163 2
        } elseif (null !== $method) {
164
            $queryBuilder->where('j.method = :method')->setParameter(':method', $method);
165
            $where = 'andWhere';
166
        }
167
168 2
        $dateTime = new \DateTime();
169
        // Filter
170
        $queryBuilder
171 2
            ->$where($queryBuilder->expr()->orX(
172 2
                $queryBuilder->expr()->isNull('j.whenAt'),
173 2
                                        $queryBuilder->expr()->lte('j.whenAt', ':whenAt')
174 2
            ))
175 2
            ->andWhere($queryBuilder->expr()->orX(
176 2
                $queryBuilder->expr()->isNull('j.expiresAt'),
177 2
                $queryBuilder->expr()->gt('j.expiresAt', ':expiresAt')
178 2
            ))
179 2
            ->andWhere('j.locked is NULL')
180 2
            ->setParameter(':whenAt', $dateTime)
181 2
            ->setParameter(':expiresAt', $dateTime);
182
183 2
        $query = $queryBuilder->getQuery();
184
185 2
        return $query->getSingleScalarResult();
186
    }
187
188
    /**
189
     * Get Jobs statuses.
190
     */
191 3
    public function getStatus()
192
    {
193 3
        $result = [];
194 3
        $this->getStatusByEntityName($this->getJobClass(), $result);
195 3
        $this->getStatusByEntityName($this->getJobArchiveClass(), $result);
196
197 3
        $finalResult = [];
198 3
        foreach ($result as $key => $item) {
199 1
            ksort($item);
200 1
            foreach ($item as $status => $count) {
201 1
                if (isset($finalResult[$key][$status])) {
202
                    $finalResult[$key][$status] += $count;
203
                } else {
204 1
                    $finalResult[$key][$status] = $count;
205
                }
206 1
            }
207 3
        }
208
209 3
        return $finalResult;
210
    }
211
212
    /**
213
     * @param string $entityName
214
     */
215 3
    protected function getStatusByEntityName($entityName, array &$result)
216
    {
217
        /** @var EntityManager $objectManager */
218 3
        $objectManager = $this->getObjectManager();
219 3
        $result1 = $objectManager->getRepository($entityName)->createQueryBuilder('j')->select('j.workerName, j.method, j.status, count(j) as c')
220 3
            ->groupBy('j.workerName, j.method, j.status')->getQuery()->getArrayResult();
221
222 3
        foreach ($result1 as $item) {
223 1
            $method = $item['workerName'].'->'.$item['method'].'()';
224 1
            if (!isset($result[$method])) {
225 1
                $result[$method] = [BaseJob::STATUS_NEW => 0,
226 1
                    BaseJob::STATUS_RUNNING => 0,
227 1
                    RetryableJob::STATUS_EXPIRED => 0,
228 1
                    RetryableJob::STATUS_MAX_ERROR => 0,
229 1
                    RetryableJob::STATUS_MAX_STALLED => 0,
230 1
                    RetryableJob::STATUS_MAX_RETRIES => 0,
231 1
                    BaseJob::STATUS_SUCCESS => 0,
232 1
                    BaseJob::STATUS_ERROR => 0, ];
233 1
            }
234 1
            $result[$method][$item['status']] += intval($item['c']);
235 3
        }
236 3
    }
237
238
    /**
239
     * Get the next job to run (can be filtered by workername and method name).
240
     *
241
     * @param string $workerName
242
     * @param string $methodName
243
     * @param bool   $prioritize
244
     * @param int    $runId
245
     *
246
     * @return Job|null
247
     */
248 11
    public function getJob($workerName = null, $methodName = null, $prioritize = true, $runId = null)
249
    {
250
        do {
251 11
            $queryBuilder = $this->getJobQueryBuilder($workerName, $methodName, $prioritize);
252 11
            $queryBuilder->select('j.id');
253 11
            $queryBuilder->setMaxResults(100);
254
255
            /** @var QueryBuilder $queryBuilder */
256 11
            $query = $queryBuilder->getQuery();
257 11
            $jobs = $query->getResult();
258 11
            if ($jobs) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $jobs of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
259 9
                foreach ($jobs as $job) {
260 9
                    if ($job = $this->takeJob($job['id'], $runId)) {
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $job is correct as $this->takeJob($job['id'], $runId) (which targets Dtc\QueueBundle\ORM\JobManager::takeJob()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
261 9
                        return $job;
262
                    }
263
                }
264
            }
265 6
        } while ($jobs);
0 ignored issues
show
Bug Best Practice introduced by
The expression $jobs of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
266
267 6
        return null;
268
    }
269
270
    /**
271
     * @param string|null $workerName
272
     * @param string|null $methodName
273
     * @param bool        $prioritize
274
     *
275
     * @return QueryBuilder
276
     */
277 11
    public function getJobQueryBuilder($workerName = null, $methodName = null, $prioritize = true)
278
    {
279
        /** @var EntityRepository $repository */
280 11
        $repository = $this->getRepository();
281 11
        $queryBuilder = $repository->createQueryBuilder('j');
282 11
        $this->addStandardPredicate($queryBuilder);
283 11
        $this->addWorkerNameCriterion($queryBuilder, $workerName, $methodName);
284
285 11
        if ($prioritize) {
286 11
            $queryBuilder->addOrderBy('j.priority', 'DESC');
287 11
            $queryBuilder->addOrderBy('j.whenAt', 'ASC');
288 11
        } else {
289 1
            $queryBuilder->orderBy('j.whenAt', 'ASC');
290
        }
291
292 11
        return $queryBuilder;
293
    }
294
295 12
    protected function addStandardPredicate(QueryBuilder $queryBuilder)
296
    {
297 12
        $dateTime = new \DateTime();
298
        $queryBuilder
299 12
            ->where('j.status = :status')->setParameter(':status', BaseJob::STATUS_NEW)
300 12
            ->andWhere('j.locked is NULL')
301 12
            ->andWhere($queryBuilder->expr()->orX(
302 12
                $queryBuilder->expr()->isNull('j.whenAt'),
303 12
                $queryBuilder->expr()->lte('j.whenAt', ':whenAt')
304 12
            ))
305 12
            ->andWhere($queryBuilder->expr()->orX(
306 12
                $queryBuilder->expr()->isNull('j.expiresAt'),
307 12
                $queryBuilder->expr()->gt('j.expiresAt', ':expiresAt')
308 12
            ))
309 12
            ->setParameter(':whenAt', $dateTime)
310 12
            ->setParameter(':expiresAt', $dateTime);
311 12
    }
312
313 10
    protected function takeJob($jobId, $runId = null)
314
    {
315
        /** @var EntityRepository $repository */
316 9
        $repository = $this->getRepository();
317
        /** @var QueryBuilder $queryBuilder */
318 9
        $queryBuilder = $repository->createQueryBuilder('j');
319
        $queryBuilder
320 9
            ->update()
321 9
            ->set('j.locked', ':locked')
322 9
            ->setParameter(':locked', true)
323 9
            ->set('j.lockedAt', ':lockedAt')
324 9
            ->setParameter(':lockedAt', new \DateTime())
325 9
            ->set('j.status', ':status')
326 9
            ->setParameter(':status', BaseJob::STATUS_RUNNING);
327 9
        if (null !== $runId) {
328
            $queryBuilder
329 1
                ->set('j.runId', ':runId')
330 1
                ->setParameter(':runId', $runId);
331 1
        }
332 9
        $queryBuilder->where('j.id = :id');
333 9
        $queryBuilder->andWhere('j.locked is NULL');
334 10
        $queryBuilder->setParameter(':id', $jobId);
335 9
        $resultCount = $queryBuilder->getQuery()->execute();
336
337 9
        if (1 === $resultCount) {
338 9
            return $repository->find($jobId);
339
        }
340
341
        return null;
342
    }
343
344
    /**
345
     * Tries to update the nearest job as a batch.
346
     *
347
     * @param \Dtc\QueueBundle\Model\Job $job
348
     *
349
     * @return null|Job
350
     */
351 1
    public function updateNearestBatch(\Dtc\QueueBundle\Model\Job $job)
352
    {
353
        /** @var QueryBuilder $queryBuilder */
354 1
        $queryBuilder = $this->getRepository()->createQueryBuilder('j');
355 1
        $queryBuilder->select()
356 1
            ->where('j.crcHash = :crcHash')
357 1
            ->andWhere('j.status = :status')
358 1
            ->setParameter(':status', BaseJob::STATUS_NEW)
359 1
            ->setParameter(':crcHash', $job->getCrcHash())
360 1
            ->orderBy('j.whenAt', 'ASC')
361 1
            ->setMaxResults(1);
362 1
        $existingJobs = $queryBuilder->getQuery()->execute();
363
364 1
        if (empty($existingJobs)) {
365
            return null;
366
        }
367
        /** @var Job $existingJob */
368 1
        $existingJob = $existingJobs[0];
369
370 1
        $newPriority = max($job->getPriority(), $existingJob->getPriority());
371 1
        $newWhenAt = min($job->getWhenAt(), $existingJob->getWhenAt());
372
373 1
        $this->updateBatchJob($existingJob, $newPriority, $newWhenAt);
374
375 1
        return $existingJob;
376
    }
377
378
    /**
379
     * @param int            $newPriority
380
     * @param null|\DateTime $newWhenAt
381
     */
382 1
    protected function updateBatchJob(Job $existingJob, $newPriority, $newWhenAt)
383
    {
384 1
        $existingPriority = $existingJob->getPriority();
385 1
        $existingWhenAt = $existingJob->getWhenAt();
386
387 1
        if ($newPriority !== $existingPriority || $newWhenAt !== $existingWhenAt) {
388
            /** @var EntityRepository $repository */
389 1
            $repository = $this->getRepository();
390
            /** @var QueryBuilder $queryBuilder */
391 1
            $queryBuilder = $repository->createQueryBuilder('j');
392 1
            $queryBuilder->update();
393 1
            if ($newPriority !== $existingPriority) {
394 1
                $existingJob->setPriority($newPriority);
395 1
                $queryBuilder->set('j.priority', ':priority')
396 1
                    ->setParameter(':priority', $newPriority);
397 1
            }
398 1
            if ($newWhenAt !== $existingWhenAt) {
399 1
                $existingJob->setWhenAt($newWhenAt);
0 ignored issues
show
Bug introduced by
It seems like $newWhenAt defined by parameter $newWhenAt on line 382 can be null; however, Dtc\QueueBundle\Model\BaseJob::setWhenAt() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
400 1
                $queryBuilder->set('j.whenAt', ':whenAt')
401 1
                    ->setParameter(':whenAt', $newWhenAt);
402 1
            }
403 1
            $queryBuilder->where('j.id = :id');
404 1
            $queryBuilder->andWhere('j.locked is NULL');
405 1
            $queryBuilder->setParameter(':id', $existingJob->getId());
406 1
            $queryBuilder->getQuery()->execute();
407 1
        }
408
409 1
        return $existingJob;
410
    }
411
412 1
    public function getWorkersAndMethods()
413
    {
414
        /** @var EntityRepository $repository */
415 1
        $repository = $this->getRepository();
416 1
        $queryBuilder = $repository->createQueryBuilder('j');
417 1
        $this->addStandardPredicate($queryBuilder);
418
        $queryBuilder
419 1
            ->select('DISTINCT j.workerName, j.method');
420
421 1
        $results = $queryBuilder->getQuery()->getArrayResult();
422 1
        if (!$results) {
423 1
            return [];
424
        }
425
        $workerMethods = [];
426
        foreach ($results as $result) {
427
            $workerMethods[$result['workerName']][] = $result['method'];
428
        }
429
430
        return $workerMethods;
431
    }
432
433
    /**
434
     * @param string $workerName
435
     * @param string $methodName
436
     */
437 2
    public function countLiveJobs($workerName = null, $methodName = null)
438
    {
439
        /** @var EntityRepository $repository */
440 2
        $repository = $this->getRepository();
441 2
        $queryBuilder = $repository->createQueryBuilder('j');
442 2
        $this->addStandardPredicate($queryBuilder);
443 2
        $this->addWorkerNameCriterion($queryBuilder, $workerName, $methodName);
444 2
        $queryBuilder->select('count(j.id)');
445
446 2
        return $queryBuilder->getQuery()->getSingleScalarResult();
447
    }
448
449
    /**
450
     * @param string   $workerName
451
     * @param string   $methodName
452
     * @param \Closure $progressCallback
453
     */
454 1
    public function archiveAllJobs($workerName = null, $methodName = null, $progressCallback)
455
    {
456
        // First mark all Live non-running jobs as Archive
457 1
        $repository = $this->getRepository();
458
        /** @var QueryBuilder $queryBuilder */
459 1
        $queryBuilder = $repository->createQueryBuilder('j');
460 1
        $queryBuilder->update($this->getJobClass(), 'j')
461 1
            ->set('j.status', ':statusArchive')
462 1
            ->setParameter(':statusArchive', Job::STATUS_ARCHIVE);
463 1
        $this->addStandardPredicate($queryBuilder);
464 1
        $this->addWorkerNameCriterion($queryBuilder, $workerName, $methodName);
465 1
        $resultCount = $queryBuilder->getQuery()->execute();
466
467 1
        if ($resultCount) {
468 1
            $this->runArchive($workerName, $methodName, $progressCallback);
469 1
        }
470 1
    }
471
472
    /**
473
     * Move jobs in 'archive' status to the archive table.
474
     *
475
     *  This is a bit of a hack to run a lower level query so as to process the INSERT INTO SELECT
476
     *   All on the server as "INSERT INTO SELECT" is not supported natively in Doctrine.
477
     *
478
     * @param string|null $workerName
479
     * @param string|null $methodName
480
     * @param \Closure $progressCallback
481
     */
482 1
    protected function runArchive($workerName = null, $methodName = null, $progressCallback)
0 ignored issues
show
Unused Code introduced by
The parameter $workerName is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $methodName is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
483
    {
484
        /** @var EntityManager $entityManager */
485 1
        $entityManager = $this->getObjectManager();
486 1
        $count = 0;
487
        do {
488
            /** @var EntityRepository $repository */
489 1
            $repository = $this->getRepository();
490 1
            $queryBuilder = $repository->createQueryBuilder('j');
491 1
            $queryBuilder->where('j.status = :status')
492 1
                ->setParameter(':status', Job::STATUS_ARCHIVE)
493 1
                ->setMaxResults(10000);
494
495 1
            $results = $queryBuilder->getQuery()->getArrayResult();
496 1
            foreach ($results as $jobRow) {
497 1
                $job = $repository->find($jobRow['id']);
498 1
                if ($job) {
499 1
                    $entityManager->remove($job);
500 1
                }
501 1
                ++$count;
502 1
                if (0 == $count % 10) {
503
                    $this->flush();
504
                    $progressCallback($count);
505
                }
506 1
            }
507 1
            $this->flush();
508 1
            $progressCallback($count);
509 1
        } while ($results && 10000 == count($results));
510 1
    }
511
}
512