├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src ├── Controllers │ ├── LaravelController.php │ └── WorkerController.php ├── Exceptions │ ├── ExpiredJobException.php │ └── MalformedRequestException.php ├── Integrations │ ├── BindsWorker.php │ ├── LaravelServiceProvider.php │ └── LumenServiceProvider.php ├── Jobs │ ├── AwsJob.php │ └── CallQueuedHandler.php └── Wrappers │ ├── DefaultWorker.php │ ├── Laravel53Worker.php │ ├── Laravel6Worker.php │ ├── Laravel8Worker.php │ └── WorkerInterface.php └── tests └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | composer.lock 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - '7.0' 5 | 6 | before_script: 7 | - composer install --dev 8 | 9 | addons: 10 | code_climate: 11 | repo_token: 989ab71e9103d594cabbd93fe473ff9c92ad76bb80c55c57d2a9c436e28a3f14 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Denis Mysenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # laravel-aws-worker 2 | [![Code Climate](https://codeclimate.com/github/dusterio/laravel-aws-worker/badges/gpa.svg)](https://codeclimate.com/github/dusterio/laravel-aws-worker/badges) 3 | [![Total Downloads](https://poser.pugx.org/dusterio/laravel-aws-worker/d/total.svg)](https://packagist.org/packages/dusterio/laravel-aws-worker) 4 | [![Latest Stable Version](https://poser.pugx.org/dusterio/laravel-aws-worker/v/stable.svg)](https://packagist.org/packages/dusterio/laravel-aws-worker) 5 | [![Latest Unstable Version](https://poser.pugx.org/dusterio/laravel-aws-worker/v/unstable.svg)](https://packagist.org/packages/dusterio/laravel-aws-worker) 6 | [![License](https://poser.pugx.org/dusterio/laravel-aws-worker/license.svg)](https://packagist.org/packages/dusterio/laravel-plain-sqs) 7 | 8 | Run Laravel tasks and queue listeners inside of AWS Elastic Beanstalk workers 9 | 10 | > We've dropped future support for Lumen, however you can still use [v0.1.40](https://github.com/dusterio/laravel-aws-worker/releases/tag/v0.1.40) for Lumen. 11 | 12 | ## Overview 13 | 14 | Laravel documentation recommends to use supervisor for queue workers and *IX cron for scheduled tasks. However, when deploying your application to AWS Elastic Beanstalk, neither option is available. 15 | 16 | This package helps you run your Laravel jobs in AWS worker environments. 17 | 18 | ![Standard Laravel queue flow](https://www.mysenko.com/images/queues-laravel.png) 19 | ![AWS Elastic Beanstalk flow](https://www.mysenko.com/images/queues-aws_eb.png) 20 | 21 | ## Dependencies 22 | 23 | * PHP >= 5.5 24 | * Laravel >= 5.1 25 | 26 | ## Scheduled tasks - option 1 27 | 28 | Option one is to use Kernel.php as the schedule and run Laravel schedule runner every minute. 29 | You remember how Laravel documentation advised you to invoke the task scheduler? Right, by running ```php artisan schedule:run``` on regular basis, and to do that we had to add an entry to our cron file: 30 | 31 | ```bash 32 | * * * * * php /path/to/artisan schedule:run >> /dev/null 2>&1 33 | ``` 34 | 35 | AWS doesn't allow you to run *IX commands or to add cron tasks directly. Instead, you have to make regular HTTP (POST, to be precise) requests to your worker endpoint. 36 | 37 | Add cron.yaml to the root folder of your application (this can be a part of your repo or you could add this file right before deploying to EB - the important thing is that this file is present at the time of deployment): 38 | 39 | ```yaml 40 | version: 1 41 | cron: 42 | - name: "schedule" 43 | url: "/worker/schedule" 44 | schedule: "* * * * *" 45 | ``` 46 | 47 | From now on, AWS will do POST /worker/schedule to your endpoint every minute - kind of the same effect we achieved when editing a UNIX cron file. The important difference here is that the worker environment still has to run a web process in order to execute scheduled tasks. 48 | Behind the scenes it will do something very similar to a built-in `schedule:run` command. 49 | 50 | Your scheduled tasks should be defined in ```App\Console\Kernel::class``` - just where they normally live in Laravel, eg.: 51 | 52 | ```php 53 | protected function schedule(Schedule $schedule) 54 | { 55 | $schedule->command('inspire') 56 | ->everyMinute(); 57 | } 58 | ``` 59 | 60 | ## Scheduled tasks - option 2 61 | 62 | Option two is to use AWS schedule defined in the cron.yml: 63 | 64 | ```yaml 65 | version: 1 66 | cron: 67 | - name: "run:command" 68 | url: "/worker/schedule" 69 | schedule: "0 * * * *" 70 | 71 | - name: "do:something --param=1 -v" 72 | url: "/worker/schedule" 73 | schedule: "*/5 * * * *" 74 | ``` 75 | 76 | Note that AWS will use UTC timezone for cron expressions. With the above example, 77 | AWS will hit /worker/schedule endpoint every hour with `run:command` artisan command and every 78 | 5 minutes with `do:something` command. Command parameters aren't supported at this stage. 79 | 80 | Pick whichever option is better for you! 81 | 82 | ## Queued jobs: SQS 83 | 84 | Normally Laravel has to poll SQS for new messages, but in case of AWS Elastic Beanstalk messages will come to us – inside of POST requests from the AWS daemon. 85 | 86 | Therefore, we will create jobs manually based on SQS payload that arrived, and pass that job to the framework's default worker. From this point, the job will be processed the way it's normally processed in Laravel. If it's processed successfully, 87 | our controller will return a 200 HTTP status and AWS daemon will delete the job from the queue. Again, we don't need to poll for jobs and we don't need to delete jobs - that's done by AWS in this case. 88 | 89 | If you dispatch jobs from another instance of Laravel or if you are following Laravel's payload format ```{"job":"","data":""}``` you should be okay to go. If you want to receive custom format JSON messages, you may want to install 90 | [Laravel plain SQS](https://github.com/dusterio/laravel-plain-sqs) package as well. 91 | 92 | ## Configuring the queue 93 | 94 | Every time you create a worker environment in AWS, you are forced to choose two SQS queues – either automatically generated ones or some of your existing queues. One of the queues will be for the jobs themselves, another one is for failed jobs – AWS calls this queue a dead letter queue. 95 | 96 | You can set your worker queues either during the environment launch or anytime later in the settings: 97 | 98 | ![AWS Worker queue settings](https://www.mysenko.com/images/worker_settings.jpg) 99 | 100 | Don't forget to set the HTTP path to ```/worker/queue``` – this is where AWS will hit our application. If you chose to generate queues automatically, you can see their details later in SQS section of the AWS console: 101 | 102 | ![AWS SQS details](https://www.mysenko.com/images/sqs_details.jpg) 103 | 104 | You have to tell Laravel about this queue. First set your queue driver to SQS in ```.env``` file: 105 | 106 | ``` 107 | QUEUE_DRIVER=sqs 108 | ``` 109 | 110 | Then go to ```config/queue.php``` and copy/paste details from AWS console: 111 | 112 | ```php 113 | ... 114 | 'sqs' => [ 115 | 'driver' => 'sqs', 116 | 'key' => 'your-public-key', 117 | 'secret' => 'your-secret-key', 118 | 'prefix' => 'https://sqs.us-east-1.amazonaws.com/your-account-id', 119 | 'queue' => 'your-queue-name', 120 | 'region' => 'us-east-1', 121 | ], 122 | ... 123 | ``` 124 | 125 | To generate key and secret go to Identity and Access Management in the AWS console. It's better to create a separate user that ONLY has access to SQS. 126 | 127 | ## Installation via Composer 128 | 129 | To install simply run: 130 | 131 | ``` 132 | composer require dusterio/laravel-aws-worker 133 | ``` 134 | 135 | Or add it to `composer.json` manually: 136 | 137 | ```json 138 | { 139 | "require": { 140 | "dusterio/laravel-aws-worker": "~0.1" 141 | } 142 | } 143 | ``` 144 | 145 | ### Usage in Laravel 5 146 | 147 | ```php 148 | // Add in your config/app.php 149 | 150 | 'providers' => [ 151 | '...', 152 | 'Dusterio\AwsWorker\Integrations\LaravelServiceProvider', 153 | ]; 154 | ``` 155 | 156 | After adding service provider, you should be able to see two special routes that we added: 157 | 158 | ```bash 159 | $ php artisan route:list 160 | +--------+----------+-----------------+------+----------------------------------------------------------+------------+ 161 | | Domain | Method | URI | Name | Action | Middleware | 162 | +--------+----------+-----------------+------+----------------------------------------------------------+------------+ 163 | | | POST | worker/queue | | Dusterio\AwsWorker\Controllers\WorkerController@queue | | 164 | | | POST | worker/schedule | | Dusterio\AwsWorker\Controllers\WorkerController@schedule | | 165 | +--------+----------+-----------------+------+----------------------------------------------------------+------------+ 166 | ``` 167 | 168 | Environment variable ```REGISTER_WORKER_ROUTES``` is used to trigger binding of the two routes above. If you run the same application in both web and worker environments, 169 | don't forget to set ```REGISTER_WORKER_ROUTES``` to ```false``` in your web environment. You don't want your regular users to be able to invoke scheduler or queue worker. 170 | 171 | This variable is set to ```true``` by default at this moment. 172 | 173 | So that's it - if you (or AWS) hits ```/worker/queue```, Laravel will process one queue item (supplied in the POST). And if you hit ```/worker/schedule```, we will run the scheduler (it's the same as to run ```php artisan schedule:run``` in shell). 174 | 175 | ### Usage in Lumen 5 176 | 177 | ```php 178 | // Add in your bootstrap/app.php 179 | $app->register(Dusterio\AwsWorker\Integrations\LumenServiceProvider::class); 180 | ``` 181 | 182 | ## Errors and exceptions 183 | 184 | Please make sure that two special routes are not mounted behind a CSRF middleware. Our POSTs are not real web forms and CSRF is not necessary here. If you have a global CSRF middleware, add these routes to exceptions, or otherwise apply CSRF to specific routes or route groups. 185 | 186 | If your job fails, we will throw a ```FailedJobException```. If you want to customize error output – just customise your exception handler. 187 | Note that your HTTP status code must be different from 200 in order for AWS to realize the job has failed. 188 | 189 | ## Job expiration (retention) 190 | 191 | A new nice feature is being able to set a job expiration (retention in AWS terms) in seconds so 192 | that low value jobs get skipped completely if there is temporary queue latency due to load. 193 | 194 | Let's say we have a spike in queued jobs and some of them don't even make sense anymore 195 | now that a few minutes passed – we don't want to spend computing resources processing them 196 | later. 197 | 198 | By setting a special property on a job or a listener class: 199 | ```php 200 | class PurgeCache implements ShouldQueue 201 | { 202 | public static int $retention = 300; // If this job is delayed more than 300 seconds, skip it 203 | } 204 | ``` 205 | 206 | We can make sure that we won't run this job later than 300 seconds since it's been queued. 207 | This is similar to AWS SQS "message retention" setting which can only be set globally for 208 | the whole queue. 209 | 210 | To use this new feature, you have to use provided ```Jobs\CallQueuedHandler``` class that 211 | extends Laravel's default ```CallQueuedHandler```. A special ```ExpiredJobException``` exception 212 | will be thrown when expired jobs arrive and then it's up to you what to do with them. 213 | 214 | If you just catch these exceptions and therefore stop Laravel from returning code 500 215 | to AWS daemon, the job will be deleted by AWS automatically. 216 | 217 | ## ToDo 218 | 219 | 1. Add support for AWS dead letter queue (retry jobs from that queue?) 220 | 221 | ## Video tutorials 222 | 223 | I've just started a educational YouTube channel that will cover top IT trends in software development and DevOps: [config.sys](https://www.youtube.com/channel/UCIvUJ1iVRjJP_xL0CD7cMpg) 224 | 225 | Also I'm glad to announce a new cool tool of mine – [GrammarCI](https://www.grammarci.com/), an automated typo/grammar checker for developers, as a part of the CI/CD pipeline. 226 | 227 | ## Implications 228 | 229 | Note that AWS cron doesn't promise 100% time accuracy. Since cron tasks share the same queue with other jobs, your scheduled tasks may be processed later than expected. 230 | 231 | ## Post scriptum 232 | 233 | I wrote a [blog post](https://web.archive.org/web/20210616063916/https://blog.menara.com.au/2016/06/running-laravel-in-amazon-elastic-beanstalk/) explaining how this actually works. 234 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dusterio/laravel-aws-worker", 3 | "type": "library", 4 | "description": "Run Laravel (or Lumen) tasks and queue listeners inside of AWS Elastic Beanstalk workers", 5 | "keywords": [ 6 | "php", 7 | "laravel", 8 | "lumen", 9 | "sqs", 10 | "aws", 11 | "elasticbeanstalk", 12 | "worker" 13 | ], 14 | "homepage": "https://github.com/dusterio/laravel-aws-worker", 15 | "license": "MIT", 16 | "authors": [ 17 | { 18 | "name": "Denis Mysenko", 19 | "email": "denis@mysenko.com", 20 | "homepage": "https://www.mysenko.com" 21 | } 22 | ], 23 | "require": { 24 | "php": ">=5.5.0", 25 | "illuminate/support": "5.*|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 26 | "illuminate/queue": "5.*|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 27 | "illuminate/bus": "5.*|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 28 | "aws/aws-sdk-php": "~3.0", 29 | "illuminate/http": "5.*|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0" 30 | }, 31 | "require-dev": { 32 | "phpunit/phpunit": "3.7.*|^9.5.10|^10.5", 33 | "codeclimate/php-test-reporter": "dev-master" 34 | }, 35 | "autoload": { 36 | "psr-4": { 37 | "Dusterio\\AwsWorker\\": "src/" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | tests/PlainSqs/ 16 | 17 | 18 | 19 | 20 | 21 | src/ 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Controllers/LaravelController.php: -------------------------------------------------------------------------------- 1 | middleware[$middleware] = $options; 24 | } 25 | 26 | /** 27 | * Get the middleware assigned to the controller. 28 | * 29 | * @return array 30 | */ 31 | public function getMiddleware() 32 | { 33 | return $this->middleware; 34 | } 35 | 36 | /** 37 | * Execute an action on the controller. 38 | * 39 | * @param string $method 40 | * @param array $parameters 41 | * @return \Symfony\Component\HttpFoundation\Response 42 | */ 43 | public function callAction($method, $parameters) 44 | { 45 | return call_user_func_array([$this, $method], $parameters); 46 | } 47 | 48 | /** 49 | * Get the registered "after" filters. 50 | * 51 | * @return array 52 | * 53 | * @deprecated since version 5.1. 54 | */ 55 | public function getAfterFilters() 56 | { 57 | return []; 58 | } 59 | 60 | /** 61 | * Get the registered "before" filters. 62 | * 63 | * @return array 64 | * 65 | * @deprecated since version 5.1. 66 | */ 67 | public function getBeforeFilters() 68 | { 69 | return []; 70 | } 71 | 72 | /** 73 | * Get the middleware for a given method. 74 | * 75 | * @param string $method 76 | * @return array 77 | */ 78 | public function getMiddlewareForMethod($method) 79 | { 80 | $middleware = []; 81 | 82 | foreach ($this->middleware as $name => $options) { 83 | if (isset($options['only']) && !in_array($method, (array)$options['only'])) { 84 | continue; 85 | } 86 | 87 | if (isset($options['except']) && in_array($method, (array)$options['except'])) { 88 | continue; 89 | } 90 | 91 | $middleware[] = $name; 92 | } 93 | 94 | return $middleware; 95 | } 96 | } 97 | 98 | -------------------------------------------------------------------------------- /src/Controllers/WorkerController.php: -------------------------------------------------------------------------------- 1 | headers->get('X-Aws-Sqsd-Taskname', $this::LARAVEL_SCHEDULE_COMMAND); 43 | if ($command != $this::LARAVEL_SCHEDULE_COMMAND) return $this->runSpecificCommand($kernel, $request->headers->get('X-Aws-Sqsd-Taskname')); 44 | 45 | $kernel->bootstrap(); 46 | 47 | $events = $schedule->dueEvents($laravel); 48 | $eventsRan = 0; 49 | $messages = []; 50 | 51 | foreach ($events as $event) { 52 | if (method_exists($event, 'filtersPass') && (new \ReflectionMethod($event, 'filtersPass'))->isPublic() && ! $event->filtersPass($laravel)) { 53 | continue; 54 | } 55 | 56 | $messages[] = 'Running: '.$event->getSummaryForDisplay(); 57 | 58 | $event->run($laravel); 59 | 60 | ++$eventsRan; 61 | } 62 | 63 | if (count($events) === 0 || $eventsRan === 0) { 64 | $messages[] = 'No scheduled commands are ready to run.'; 65 | } 66 | 67 | return $this->response($messages); 68 | } 69 | 70 | /** 71 | * @param string $command 72 | * @return array 73 | */ 74 | protected function parseCommand($command) 75 | { 76 | $elements = explode(' ', $command); 77 | $name = $elements[0]; 78 | if (count($elements) == 1) return [$name, []]; 79 | 80 | array_shift($elements); 81 | $arguments = []; 82 | 83 | array_map(function($parameter) use (&$arguments) { 84 | if (strstr($parameter, '=')) { 85 | $parts = explode('=', $parameter); 86 | $arguments[$parts[0]] = $parts[1]; 87 | return; 88 | } 89 | 90 | $arguments[$parameter] = true; 91 | }, $elements); 92 | 93 | return [ 94 | $name, 95 | $arguments 96 | ]; 97 | } 98 | 99 | /** 100 | * @param Kernel $kernel 101 | * @param $command 102 | * @return Response 103 | */ 104 | protected function runSpecificCommand(Kernel $kernel, $command) 105 | { 106 | list ($name, $arguments) = $this->parseCommand($command); 107 | 108 | $exitCode = $kernel->call($name, $arguments); 109 | 110 | return $this->response($exitCode); 111 | } 112 | 113 | /** 114 | * @param Request $request 115 | * @param WorkerInterface $worker 116 | * @param Container $laravel 117 | * @param ExceptionHandler $exceptions 118 | * @return Response 119 | */ 120 | public function queue(Request $request, WorkerInterface $worker, Container $laravel, ExceptionHandler $exceptions) 121 | { 122 | //$this->validateHeaders($request); 123 | $body = $this->validateBody($request, $laravel); 124 | 125 | $job = new AwsJob($laravel, $request->header('X-Aws-Sqsd-Queue'), [ 126 | 'Body' => $body, 127 | 'MessageId' => $request->header('X-Aws-Sqsd-Msgid'), 128 | 'ReceiptHandle' => false, 129 | 'Attributes' => [ 130 | 'ApproximateReceiveCount' => $request->header('X-Aws-Sqsd-Receive-Count'), 131 | 'SentTimestamp' => $request->headers->has('X-Aws-Sqsd-First-Received-At') ? strtotime($request->header('X-Aws-Sqsd-First-Received-At')) * 1000 : null 132 | ] 133 | ]); 134 | 135 | $worker->process( 136 | $request->header('X-Aws-Sqsd-Queue'), $job, array_merge([ 137 | 'delay' => 0 138 | ], $this->tryToExtractOptions($body)) 139 | ); 140 | 141 | return $this->response([ 142 | 'Processed ' . $job->getJobId() 143 | ]); 144 | } 145 | 146 | /** 147 | * @param $input 148 | * @return int 149 | */ 150 | private function tryToExtractOptions($input) 151 | { 152 | $parameters = [ 153 | 'maxTries' => 0, 154 | 'timeout' => 60 155 | ]; 156 | 157 | // Try to extract listener class options from the serialized job body 158 | if (preg_match('/tries\\\\\\\\\\\\";i:(?\d+);/', $input, $matches)) { 159 | $parameters['maxTries'] = intval($matches['attempts']); 160 | } 161 | 162 | if (preg_match('/timeout\\\\\\\\\\\\";i:(?\d+);/', $input, $matches)) { 163 | $parameters['timeout'] = intval($matches['timeout']); 164 | } 165 | 166 | return $parameters; 167 | } 168 | 169 | /** 170 | * @param Request $request 171 | * @throws MalformedRequestException 172 | */ 173 | private function validateHeaders(Request $request) 174 | { 175 | foreach ($this->awsHeaders as $header) { 176 | if (! $this->hasHeader($request, $header)) { 177 | throw new MalformedRequestException('Missing AWS header: ' . $header); 178 | } 179 | } 180 | } 181 | 182 | /** 183 | * @param Request $request 184 | * @param $header 185 | * @return bool 186 | */ 187 | private function hasHeader(Request $request, $header) 188 | { 189 | if (method_exists($request, 'hasHeader')) { 190 | return $request->hasHeader($header); 191 | } 192 | 193 | return $request->header($header, false); 194 | } 195 | 196 | /** 197 | * @param Request $request 198 | * @param Container $laravel 199 | * @return string 200 | * @throws MalformedRequestException 201 | */ 202 | private function validateBody(Request $request, Container $laravel) 203 | { 204 | if (empty($request->getContent())) { 205 | throw new MalformedRequestException('Empty request body'); 206 | } 207 | 208 | $job = json_decode($request->getContent(), true); 209 | if ($job === null) throw new MalformedRequestException('Unable to decode request JSON'); 210 | 211 | if (isset($job['job']) && isset($job['data'])) return $request->getContent(); 212 | 213 | // If the format is not the standard Laravel format, try to mimic it 214 | $queueId = explode('/', $request->header('X-Aws-Sqsd-Queue')); 215 | $queueId = array_pop($queueId); 216 | 217 | $class = (array_key_exists($queueId, $laravel['config']->get('sqs-plain.handlers'))) 218 | ? $laravel['config']->get('sqs-plain.handlers')[$queueId] 219 | : $laravel['config']->get('sqs-plain.default-handler'); 220 | 221 | return json_encode([ 222 | 'job' => $class . '@handle', 223 | 'data' => $request->getContent() 224 | ]); 225 | } 226 | 227 | /** 228 | * @param array $messages 229 | * @param int $code 230 | * @return Response 231 | */ 232 | private function response($messages = [], $code = 200) 233 | { 234 | return new Response(json_encode([ 235 | 'message' => $messages, 236 | 'code' => $code 237 | ]), $code); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/Exceptions/ExpiredJobException.php: -------------------------------------------------------------------------------- 1 | Laravel53Worker::class, 24 | '[67]\.\d+\.\d+' => Laravel6Worker::class, 25 | '([89]|10|11|12)\.\d+\.\d+' => Laravel8Worker::class 26 | ]; 27 | 28 | /** 29 | * @param $version 30 | * @return mixed 31 | */ 32 | protected function findWorkerClass($version) 33 | { 34 | foreach ($this->workerImplementations as $regexp => $class) { 35 | if (preg_match('/' . $regexp . '/', $version)) return $class; 36 | } 37 | 38 | return DefaultWorker::class; 39 | } 40 | 41 | /** 42 | * @return void 43 | */ 44 | protected function bindWorker() 45 | { 46 | // If Laravel version is 6 or above then the worker bindings change. So we initiate it here 47 | if ((int) $this->app->version() >= 6) { 48 | $this->app->singleton(Worker::class, function () { 49 | $isDownForMaintenance = function () { 50 | return $this->app->isDownForMaintenance(); 51 | }; 52 | 53 | return new Worker( 54 | $this->app['queue'], 55 | $this->app['events'], 56 | $this->app[ExceptionHandler::class], 57 | $isDownForMaintenance 58 | ); 59 | }); 60 | } 61 | 62 | $this->app->bind(WorkerInterface::class, $this->findWorkerClass($this->app->version())); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Integrations/LaravelServiceProvider.php: -------------------------------------------------------------------------------- 1 | bindWorker(); 30 | $this->addRoutes(); 31 | } 32 | 33 | /** 34 | * @return void 35 | */ 36 | protected function addRoutes() 37 | { 38 | $this->app['router']->post('/worker/schedule', 'Dusterio\AwsWorker\Controllers\WorkerController@schedule'); 39 | $this->app['router']->post('/worker/queue', 'Dusterio\AwsWorker\Controllers\WorkerController@queue'); 40 | } 41 | 42 | /** 43 | * @return void 44 | */ 45 | public function boot() 46 | { 47 | $this->app->singleton(QueueManager::class, function() { 48 | return new QueueManager($this->app); 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Integrations/LumenServiceProvider.php: -------------------------------------------------------------------------------- 1 | bindWorker(); 29 | $this->addRoutes(isset( $this->app->router ) ? $this->app->router : $this->app ); 30 | } 31 | 32 | /** 33 | * @param mixed $router 34 | * @return void 35 | */ 36 | protected function addRoutes($router) 37 | { 38 | $router->post('/worker/schedule', 'Dusterio\AwsWorker\Controllers\WorkerController@schedule'); 39 | $router->post('/worker/queue', 'Dusterio\AwsWorker\Controllers\WorkerController@queue'); 40 | } 41 | 42 | /** 43 | * @return void 44 | */ 45 | public function boot() 46 | { 47 | $this->app->singleton(QueueManager::class, function() { 48 | return new QueueManager($this->app); 49 | }); 50 | 51 | // If lumen version is 6 or above then the worker bindings change. So we initiate it here 52 | if (preg_match('/Lumen \(6/', $this->app->version())) { 53 | $this->app->singleton(Worker::class, function () { 54 | $isDownForMaintenance = function () { 55 | return $this->app->isDownForMaintenance(); 56 | }; 57 | 58 | return new Worker( 59 | $this->app['queue'], 60 | $this->app['events'], 61 | $this->app[ExceptionHandler::class], 62 | $isDownForMaintenance 63 | ); 64 | }); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Jobs/AwsJob.php: -------------------------------------------------------------------------------- 1 | job = $job; 30 | $this->queue = $queue; 31 | $this->container = $container; 32 | } 33 | 34 | /** 35 | * Fire the job. 36 | * 37 | * @return void 38 | */ 39 | public function fire() 40 | { 41 | if (method_exists($this, 'resolveAndFire')) { 42 | $this->resolveAndFire(json_decode($this->getRawBody(), true)); 43 | return; 44 | } 45 | 46 | parent::fire(); 47 | } 48 | 49 | /** 50 | * Get the raw body string for the job. 51 | * 52 | * @return string 53 | */ 54 | public function getRawBody() 55 | { 56 | return $this->job['Body']; 57 | } 58 | 59 | /** 60 | * Actually, AWS will do this for us, we just need to mark the job as deleted 61 | * 62 | * @return void 63 | */ 64 | public function delete() 65 | { 66 | parent::delete(); 67 | } 68 | 69 | /** 70 | * AWS daemon will do this for us 71 | * 72 | * @param int $delay 73 | * @return void 74 | */ 75 | public function release($delay = 0) 76 | { 77 | parent::release($delay); 78 | } 79 | 80 | /** 81 | * Get the number of times the job has been attempted. 82 | * 83 | * @return int 84 | */ 85 | public function attempts() 86 | { 87 | return (int) $this->job['Attributes']['ApproximateReceiveCount']; 88 | } 89 | 90 | /** 91 | * Get the UNIX timestamp of the message creation. 92 | * 93 | * @return int 94 | */ 95 | public function timestamp() 96 | { 97 | return (int) round($this->job['Attributes']['SentTimestamp'] / 1000); 98 | } 99 | 100 | /** 101 | * Get the job identifier. 102 | * 103 | * @return string 104 | */ 105 | public function getJobId() 106 | { 107 | return $this->job['MessageId']; 108 | } 109 | 110 | /** 111 | * Get the IoC container instance. 112 | * 113 | * @return \Illuminate\Container\Container 114 | */ 115 | public function getContainer() 116 | { 117 | return $this->container; 118 | } 119 | 120 | /** 121 | * We don't need an underlying SQS client instance. 122 | * 123 | * @return \Aws\Sqs\SqsClient 124 | */ 125 | public function getSqs() 126 | { 127 | return null; 128 | } 129 | 130 | /** 131 | * Get the underlying raw SQS job. 132 | * 133 | * @return array 134 | */ 135 | public function getSqsJob() 136 | { 137 | return $this->job; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Jobs/CallQueuedHandler.php: -------------------------------------------------------------------------------- 1 | hasExpired($command->class, $job->timestamp())) { 20 | throw new ExpiredJobException("Job {$command->class} has already expired"); 21 | } 22 | 23 | return parent::dispatchThroughMiddleware($job, $command); 24 | } 25 | 26 | /** 27 | * @param $className 28 | * @param $queuedAt 29 | * @return bool 30 | */ 31 | protected function hasExpired($className, $queuedAt) { 32 | if (! property_exists($className, 'retention')) { 33 | return false; 34 | } 35 | 36 | return time() > $queuedAt + $className::$retention; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Wrappers/DefaultWorker.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 33 | $this->worker = $worker; 34 | } 35 | 36 | /** 37 | * @param $queue 38 | * @param $job 39 | * @param array $options 40 | * @return void 41 | */ 42 | public function process($queue, $job, array $options) 43 | { 44 | $workerOptions = new WorkerOptions('default', $options['delay'], 128, $options['timeout'], 3, $options['maxTries']); 45 | 46 | $this->worker->process( 47 | $queue, $job, $workerOptions 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Wrappers/Laravel53Worker.php: -------------------------------------------------------------------------------- 1 | worker = $worker; 21 | } 22 | 23 | /** 24 | * @param $queue 25 | * @param $job 26 | * @param array $options 27 | * @return void 28 | */ 29 | public function process($queue, $job, array $options) 30 | { 31 | $workerOptions = new WorkerOptions($options['delay'], 128, 60, 3, $options['maxTries']); 32 | 33 | $this->worker->process( 34 | $queue, $job, $workerOptions 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Wrappers/Laravel6Worker.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 23 | $this->worker = $worker; 24 | } 25 | 26 | /** 27 | * @param $queue 28 | * @param $job 29 | * @param array $options 30 | * @return void 31 | */ 32 | public function process($queue, $job, array $options) 33 | { 34 | $workerOptions = new WorkerOptions($options['delay'], 128, 60, 3, $options['maxTries']); 35 | 36 | $this->worker->process( 37 | $queue, $job, $workerOptions 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Wrappers/Laravel8Worker.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 33 | $this->worker = $worker; 34 | } 35 | 36 | /** 37 | * @param $queue 38 | * @param $job 39 | * @param array $options 40 | * @return void 41 | */ 42 | public function process($queue, $job, array $options) 43 | { 44 | $workerOptions = new WorkerOptions('default', $options['delay'], 128, $options['timeout'], 3, $options['maxTries']); 45 | 46 | $this->worker->process( 47 | $queue, $job, $workerOptions 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Wrappers/WorkerInterface.php: -------------------------------------------------------------------------------- 1 |