diff --git a/README.md b/README.md index e3da392cb..dbbb4e3aa 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,11 @@ And add the service provider in `config/app.php`: Jenssegers\Mongodb\MongodbServiceProvider::class, ``` +For job queue support add the optional: +```php +Jenssegers\Mongodb\MongodbQueueServiceProvider::class, +``` + For usage with [Lumen](http://lumen.laravel.com), add the service provider in `bootstrap/app.php`. In this file, you will also need to enable Eloquent. You must however ensure that your call to `$app->withEloquent();` is **below** where you have registered the `MongodbServiceProvider`: ```php @@ -100,6 +105,8 @@ You can connect to multiple servers or replica sets with the following configura ), ``` +You also need to set your QUEUE_DRIVER to 'database' in your environment or .env + Eloquent -------- diff --git a/src/Jenssegers/Mongodb/MongodbQueueServiceProvider.php b/src/Jenssegers/Mongodb/MongodbQueueServiceProvider.php new file mode 100644 index 000000000..112d6f426 --- /dev/null +++ b/src/Jenssegers/Mongodb/MongodbQueueServiceProvider.php @@ -0,0 +1,29 @@ +app->resolving('queue', function ($queue) + { + $queue->extend('database', function () { + return new MongodbConnector($this->app['db']); + }); + }); + } + +} diff --git a/src/Jenssegers/Mongodb/MongodbServiceProvider.php b/src/Jenssegers/Mongodb/MongodbServiceProvider.php index 764fb540f..5db378e81 100644 --- a/src/Jenssegers/Mongodb/MongodbServiceProvider.php +++ b/src/Jenssegers/Mongodb/MongodbServiceProvider.php @@ -26,6 +26,7 @@ public function register() return new Connection($config); }); }); + } } diff --git a/src/Jenssegers/Mongodb/Queue/MongodbConnector.php b/src/Jenssegers/Mongodb/Queue/MongodbConnector.php new file mode 100644 index 000000000..fd4b719ff --- /dev/null +++ b/src/Jenssegers/Mongodb/Queue/MongodbConnector.php @@ -0,0 +1,44 @@ +connections = $connections; + } + + /** + * Establish a queue connection. + * + * @param array $config + * @return \Illuminate\Contracts\Queue\Queue + */ + public function connect(array $config) + { + return new MongodbQueue( + $this->connections->connection(Arr::get($config, 'connection')), + $config['table'], + $config['queue'], + Arr::get($config, 'expire', 60) + ); + } +} diff --git a/src/Jenssegers/Mongodb/Queue/MongodbJob.php b/src/Jenssegers/Mongodb/Queue/MongodbJob.php new file mode 100644 index 000000000..a4da53ada --- /dev/null +++ b/src/Jenssegers/Mongodb/Queue/MongodbJob.php @@ -0,0 +1,139 @@ +job = $job; + $this->queue = $queue; + $this->database = $database; + $this->container = $container; + $this->job->attempts = $this->job->attempts + 1; + } + + /** + * Fire the job. + * + * @return void + */ + public function fire() + { + $this->resolveAndFire(json_decode($this->job->payload, true)); + } + + /** + * Delete the job from the queue. + * + * @return void + */ + public function delete() + { + parent::delete(); + + $this->database->deleteReserved($this->queue, $this->job->_id); + } + + /** + * Release the job back into the queue. + * + * @param int $delay + * @return void + */ + public function release($delay = 0) + { + parent::release($delay); + + $this->delete(); + + $this->database->release($this->queue, $this->job, $delay); + } + + /** + * Get the number of times the job has been attempted. + * + * @return int + */ + public function attempts() + { + return (int) $this->job->attempts; + } + + /** + * Get the job identifier. + * + * @return string + */ + public function getJobId() + { + return $this->job->id; + } + + /** + * Get the raw body string for the job. + * + * @return string + */ + public function getRawBody() + { + return $this->job->payload; + } + + /** + * Get the IoC container instance. + * + * @return \Illuminate\Container\Container + */ + public function getContainer() + { + return $this->container; + } + + /** + * Get the underlying queue driver instance. + * + * @return \Illuminate\Queue\DatabaseQueue + */ + public function getDatabaseQueue() + { + return $this->database; + } + + /** + * Get the underlying database job. + * + * @return \StdClass + */ + public function getDatabaseJob() + { + return $this->job; + } +} diff --git a/src/Jenssegers/Mongodb/Queue/MongodbQueue.php b/src/Jenssegers/Mongodb/Queue/MongodbQueue.php new file mode 100644 index 000000000..a5683fc50 --- /dev/null +++ b/src/Jenssegers/Mongodb/Queue/MongodbQueue.php @@ -0,0 +1,332 @@ +table = $table; + $this->expire = $expire; + $this->default = $default; + $this->database = $database; + } + + /** + * Push a new job onto the queue. + * + * @param string $job + * @param mixed $data + * @param string $queue + * @return void + */ + public function push($job, $data = '', $queue = null) + { + return $this->pushToDatabase(0, $queue, $this->createPayload($job, $data)); + } + + /** + * Push a raw payload onto the queue. + * + * @param string $payload + * @param string $queue + * @param array $options + * @return mixed + */ + public function pushRaw($payload, $queue = null, array $options = []) + { + return $this->pushToDatabase(0, $queue, $payload); + } + + /** + * Push a new job onto the queue after a delay. + * + * @param \DateTime|int $delay + * @param string $job + * @param mixed $data + * @param string $queue + * @return void + */ + public function later($delay, $job, $data = '', $queue = null) + { + return $this->pushToDatabase($delay, $queue, $this->createPayload($job, $data)); + } + + /** + * Push an array of jobs onto the queue. + * + * @param array $jobs + * @param mixed $data + * @param string $queue + * @return mixed + */ + public function bulk($jobs, $data = '', $queue = null) + { + $queue = $this->getQueue($queue); + + $availableAt = $this->getAvailableAt(0); + + $records = array_map(function ($job) use ($queue, $data, $availableAt) { + return $this->buildDatabaseRecord( + $queue, $this->createPayload($job, $data), $availableAt + ); + }, (array) $jobs); + + return $this->database->table($this->table)->insert($records); + } + + /** + * Release a reserved job back onto the queue. + * + * @param string $queue + * @param \StdClass $job + * @param int $delay + * @return void + */ + public function release($queue, $job, $delay) + { + return $this->pushToDatabase($delay, $queue, $job->payload, $job->attempts); + } + + /** + * Push a raw payload to the database with a given delay. + * + * @param \DateTime|int $delay + * @param string|null $queue + * @param string $payload + * @param int $attempts + * @return mixed + */ + protected function pushToDatabase($delay, $queue, $payload, $attempts = 0) + { + $attributes = $this->buildDatabaseRecord( + $this->getQueue($queue), $payload, $this->getAvailableAt($delay), $attempts + ); + + return $this->database->table($this->table)->insertGetId($attributes); + } + + /** + * Pop the next job off of the queue. + * + * @param string $queue + * @return \Illuminate\Contracts\Queue\Job|null + */ + public function pop($queue = null) + { + $queue = $this->getQueue($queue); + + if (! is_null($this->expire)) { + $this->releaseJobsThatHaveBeenReservedTooLong($queue); + } + + if ($job = $this->getNextAvailableJob($queue)) { + $this->markJobAsReserved($job->_id); + + return new MongodbJob( + $this->container, $this, $job, $queue + ); + } + + $this->database->commit(); + } + + /** + * Release the jobs that have been reserved for too long. + * + * @param string $queue + * @return void + */ + protected function releaseJobsThatHaveBeenReservedTooLong($queue) + { + $expired = Carbon::now()->subSeconds($this->expire)->getTimestamp(); + $reserved = $this->database->collection($this->table) + ->where('queue', $this->getQueue($queue)) + ->where('reserved', 1) + ->where('reserved_at', '<=', $expired)->get(); + + foreach ($reserved as $job) { + $attempts = $job['attempts'] + 1; + $this->releaseJob($job['_id'], $attempts); + } + } + + /** + * Get the next available job for the queue. + * + * @param string|null $queue + * @return \StdClass|null + */ + protected function getNextAvailableJob($queue) + { + $job = $this->database->table($this->table) + ->lockForUpdate() + ->where('queue', $this->getQueue($queue)) + ->where('reserved', 0) + ->where('available_at', '<=', $this->getTime()) + ->orderBy('_id', 'asc') + ->first(); + + return $job ? (object) $job : null; + } + + /** + * Mark the given job ID as reserved. + * + * @param string $id + * @return void + */ + protected function markJobAsReserved($id) + { + $this->database->table($this->table)->where('_id', $id)->update([ + 'reserved' => 1, 'reserved_at' => $this->getTime(), + ]); + } + + /** + * Release the given job ID from reservation. + * + * @param string $id + * @return void + */ + protected function releaseJob($id, $attempts) + { + $this->database->table($this->table)->where('_id', $id)->update([ + 'reserved' => 0, + 'reserved_at' => null, + 'attempts' => $attempts, + ]); + } + + /** + * Delete a reserved job from the queue. + * + * @param string $queue + * @param string $id + * @return void + */ + public function deleteReserved($queue, $id) + { + $this->database->table($this->table)->where('_id', $id)->delete(); + } + + /** + * Get the "available at" UNIX timestamp. + * + * @param \DateTime|int $delay + * @return int + */ + protected function getAvailableAt($delay) + { + $availableAt = $delay instanceof DateTime ? $delay : Carbon::now()->addSeconds($delay); + + return $availableAt->getTimestamp(); + } + + /** + * Create an array to insert for the given job. + * + * @param string|null $queue + * @param string $payload + * @param int $availableAt + * @param int $attempts + * @return array + */ + protected function buildDatabaseRecord($queue, $payload, $availableAt, $attempts = 0) + { + return [ + 'queue' => $queue, + 'payload' => $payload, + 'attempts' => $attempts, + 'reserved' => 0, + 'reserved_at' => null, + 'available_at' => $availableAt, + 'created_at' => $this->getTime(), + ]; + } + + /** + * Get the queue or return the default. + * + * @param string|null $queue + * @return string + */ + protected function getQueue($queue) + { + return $queue ?: $this->default; + } + + /** + * Get the underlying database instance. + * + * @return \Illuminate\Database\Connection + */ + public function getDatabase() + { + return $this->database; + } + + /** + * Get the expiration time in seconds. + * + * @return int|null + */ + public function getExpire() + { + return $this->expire; + } + + /** + * Set the expiration time in seconds. + * + * @param int|null $seconds + * @return void + */ + public function setExpire($seconds) + { + $this->expire = $seconds; + } +}