├── LICENSE ├── README.md ├── composer.json ├── config └── bref.php ├── src ├── BrefServiceProvider.php ├── HandlerResolver.php ├── Http │ ├── HttpHandler.php │ ├── Middleware │ │ └── ServeStaticAssets.php │ ├── OctaneHandler.php │ └── SymfonyRequestBridge.php ├── LaravelPathFinder.php ├── MaintenanceMode.php ├── Octane │ └── OctaneClient.php ├── Queue │ ├── QueueHandler.php │ ├── SqsJob.php │ └── Worker.php ├── StorageDirectories.php └── bref-init.php └── stubs ├── 503.html ├── 503.json └── serverless.yml /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 CacheWerk 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 | # Bref Laravel Bridge 2 | 3 | Run Laravel on AWS Lambda with [Bref](https://bref.sh/). 4 | 5 | Read the [Bref documentation for Laravel](https://bref.sh/docs/frameworks/laravel.html) to get started. 6 | 7 | ## Background 8 | 9 | This package was originally created by [CacheWerk](https://github.com/cachewerk/) (the creators of [Relay](https://relay.so)), maintained by [Till Krüss](https://github.com/tillkruss) and [George Boot](https://github.com/georgeboot). It was published at [cachewerk/bref-laravel-bridge](https://github.com/cachewerk/bref-laravel-bridge). 10 | 11 | For Bref 2.0, the contributors joined the Bref organization and CacheWerk's bridge was merged into this repository to create v2.0 of the bridge. 12 | 13 | ## Documentation 14 | 15 | The documentation is available at [bref.sh/docs/frameworks/laravel.html](https://bref.sh/docs/frameworks/laravel.html). 16 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bref/laravel-bridge", 3 | "description": "An advanced Laravel integration for Bref, including Octane support.", 4 | "license": "MIT", 5 | "homepage": "https://bref.sh/docs/frameworks/laravel.html", 6 | "keywords": [ 7 | "bref", 8 | "serverless", 9 | "aws", 10 | "lambda", 11 | "faas" 12 | ], 13 | "require": { 14 | "php": "^8.0", 15 | "aws/aws-sdk-php": "^3.222", 16 | "bref/bref": "^2.1.8", 17 | "bref/laravel-health-check": "^1", 18 | "illuminate/container": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0", 19 | "illuminate/contracts": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0", 20 | "illuminate/http": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0", 21 | "illuminate/queue": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0", 22 | "illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0", 23 | "laravel/octane": "^1.2 || ^2.0", 24 | "riverline/multipart-parser": "^2.0" 25 | }, 26 | "require-dev": { 27 | "nunomaduro/larastan": "^2.2", 28 | "orchestra/testbench": "^7.13 || ^9.0 || ^10.0", 29 | "php-parallel-lint/php-parallel-lint": "^1.3", 30 | "phpunit/phpunit": "^9.5", 31 | "squizlabs/php_codesniffer": "^3.7" 32 | }, 33 | "minimum-stability": "dev", 34 | "prefer-stable": true, 35 | "autoload": { 36 | "psr-4": { 37 | "Bref\\LaravelBridge\\": "src" 38 | }, 39 | "files": [ 40 | "src/bref-init.php" 41 | ] 42 | }, 43 | "config": { 44 | "sort-packages": true 45 | }, 46 | "extra": { 47 | "laravel": { 48 | "providers": [ 49 | "Bref\\LaravelBridge\\BrefServiceProvider" 50 | ] 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /config/bref.php: -------------------------------------------------------------------------------- 1 | [ 17 | // 'favicon.ico', 18 | // 'robots.txt', 19 | ], 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Shared Log Context 24 | |-------------------------------------------------------------------------- 25 | | 26 | | In order to make debugging a little easier, the Lambda `X-Request-ID` 27 | | value can be added to the shared log context automatically. 28 | | 29 | */ 30 | 31 | 'request_context' => false, 32 | 33 | /* 34 | |-------------------------------------------------------------------------- 35 | | Jobs Logging 36 | |-------------------------------------------------------------------------- 37 | | 38 | | Here you can disable detailed logging of every job execution. 39 | | 40 | */ 41 | 42 | 'log_jobs' => true, 43 | ]; 44 | -------------------------------------------------------------------------------- /src/BrefServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom(__DIR__ . '/../config/bref.php', 'bref'); 33 | $this->shareRequestContext(); 34 | 35 | if (! isset($_SERVER['LAMBDA_TASK_ROOT'])) { 36 | return; 37 | } 38 | 39 | $this->app->useStoragePath(StorageDirectories::Path); 40 | 41 | $this->fixDefaultConfiguration(); 42 | 43 | Config::set('app.mix_url', Config::get('app.asset_url')); 44 | 45 | Config::set('trustedproxy.proxies', ['0.0.0.0/0', '2000:0:0:0:0:0:0:0/3']); 46 | 47 | Config::set('view.compiled', StorageDirectories::Path . '/framework/views'); 48 | Config::set('cache.stores.file.path', StorageDirectories::Path . '/framework/cache'); 49 | 50 | $this->fixAwsCredentialsConfig(); 51 | 52 | $this->app->when(QueueHandler::class) 53 | ->needs('$connection') 54 | ->giveConfig('queue.default'); 55 | } 56 | 57 | /** 58 | * Bootstrap package services. 59 | * 60 | * @return void 61 | */ 62 | public function boot(Dispatcher $dispatcher, LogManager $logManager, FailedJobProviderInterface $queueFailer) 63 | { 64 | $this->app[Kernel::class]->pushMiddleware(Http\Middleware\ServeStaticAssets::class); 65 | 66 | if ($this->app->runningInConsole()) { 67 | $this->publishes([ 68 | __DIR__ . '/../stubs/serverless.yml' => base_path('serverless.yml'), 69 | ], 'serverless-config'); 70 | 71 | $this->publishes([ 72 | __DIR__ . '/../config/bref.php' => config_path('bref.php'), 73 | ], 'bref-config'); 74 | } 75 | 76 | if (config('bref.log_jobs', true)) { 77 | $this->enableDetailedJobLogging($dispatcher, $logManager, $queueFailer); 78 | } 79 | 80 | if (file_exists('/proc/1/fd/1')) { 81 | $dispatcher->listen( 82 | ScheduledTaskStarting::class, 83 | fn(ScheduledTaskStarting $task) => $task->task->appendOutputTo('/proc/1/fd/1'), 84 | ); 85 | } 86 | } 87 | 88 | private function enableDetailedJobLogging( 89 | Dispatcher $dispatcher, 90 | LogManager $logManager, 91 | FailedJobProviderInterface $queueFailer 92 | ): void { 93 | $dispatcher->listen( 94 | fn (JobProcessing $event) => $logManager->info( 95 | "Processing job {$event->job->getJobId()}", 96 | ['name' => $event->job->resolveName()] 97 | ) 98 | ); 99 | 100 | $dispatcher->listen( 101 | fn (JobProcessed $event) => $logManager->info( 102 | "Processed job {$event->job->getJobId()}", 103 | ['name' => $event->job->resolveName()] 104 | ) 105 | ); 106 | 107 | $dispatcher->listen( 108 | fn (JobExceptionOccurred $event) => $logManager->error( 109 | "Job failed {$event->job->getJobId()}", 110 | ['name' => $event->job->resolveName()] 111 | ) 112 | ); 113 | 114 | $dispatcher->listen( 115 | fn (JobFailed $event) => $queueFailer->log( 116 | $event->connectionName, 117 | $event->job->getQueue(), 118 | $event->job->getRawBody(), 119 | $event->exception 120 | ) 121 | ); 122 | } 123 | 124 | /** 125 | * Add the request identifier to the shared log context. 126 | * 127 | * @return void 128 | */ 129 | protected function shareRequestContext() 130 | { 131 | if (! Config::get('bref.request_context')) { 132 | return; 133 | } 134 | 135 | $this->app->rebinding('request', function ($app, $request) { 136 | if ($request->hasHeader('X-Request-ID')) { 137 | $app->make(LogManager::class)->shareContext([ 138 | 'requestId' => $request->header('X-Request-ID'), 139 | ]); 140 | } 141 | }); 142 | } 143 | 144 | /** 145 | * Prevent the default Laravel configuration from causing errors. 146 | * 147 | * @return void 148 | */ 149 | protected function fixDefaultConfiguration() 150 | { 151 | if (Config::get('session.driver') === 'file') { 152 | Config::set('session.driver', 'cookie'); 153 | } 154 | 155 | if (Config::get('logging.default') === 'stack') { 156 | Config::set('logging.default', 'stderr'); 157 | } 158 | 159 | if (Config::get('logging.channels.emergency.path') === storage_path('logs/laravel.log')) { 160 | Config::set('logging.channels.emergency', Config::get('logging.channels.stderr')); 161 | } 162 | } 163 | 164 | private function fixAwsCredentialsConfig(): void 165 | { 166 | $accessKeyId = $_SERVER['AWS_ACCESS_KEY_ID'] ?? null; 167 | $sessionToken = $_SERVER['AWS_SESSION_TOKEN'] ?? null; 168 | // If we are not in a Lambda environment, we don't need to do anything 169 | if (!$accessKeyId || ! $sessionToken) { 170 | return; 171 | } 172 | 173 | // Patch SQS config 174 | foreach (Config::get('queue.connections') as $name => $connection) { 175 | if ($connection['driver'] !== 'sqs') { 176 | continue; 177 | } 178 | // If a different key is in the config than in the environment variables 179 | if (isset($connection['key']) && $connection['key'] !== $accessKeyId) { 180 | continue; 181 | } 182 | 183 | Config::set("queue.connections.$name.token", $sessionToken); 184 | } 185 | 186 | // Patch SQS failed jobs config when using DynamoDB 187 | $failedConfig = Config::get('queue.failed'); 188 | if (isset($failedConfig['driver']) && $failedConfig['driver'] === 'dynamodb' && $failedConfig['key'] === $accessKeyId) { 189 | Config::set('queue.failed.token', $sessionToken); 190 | } 191 | 192 | // Patch S3 config 193 | foreach (Config::get('filesystems.disks') as $name => $disk) { 194 | if ($disk['driver'] !== 's3') { 195 | continue; 196 | } 197 | // If a different key is in the config than in the environment variables 198 | if (isset($disk['key']) && $disk['key'] !== $accessKeyId) { 199 | continue; 200 | } 201 | 202 | Config::set("filesystems.disks.$name.token", $sessionToken); 203 | } 204 | 205 | // Patch DynamoDB config 206 | foreach (Config::get('cache.stores') as $name => $store) { 207 | if ($store['driver'] !== 'dynamodb') { 208 | continue; 209 | } 210 | // If a different key is in the config than in the environment variables 211 | if (isset($store['key']) && $store['key'] !== $accessKeyId) { 212 | continue; 213 | } 214 | 215 | Config::set("cache.stores.$name.token", $sessionToken); 216 | } 217 | 218 | // Patch SES config 219 | if (Config::get('services.ses.key') === $accessKeyId) { 220 | Config::set('services.ses.token', $sessionToken); 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/HandlerResolver.php: -------------------------------------------------------------------------------- 1 | fileLocator = new FileHandlerLocator; 30 | $this->laravel = null; 31 | } 32 | 33 | public function get(string $id) 34 | { 35 | // By default, we check if the handler is a file name (classic Bref behavior) 36 | if ($this->fileLocator->has($id)) { 37 | return $this->fileLocator->get($id); 38 | } 39 | 40 | // The Octane handler is special: it is not created by Laravel's container 41 | if ($id === OctaneHandler::class) { 42 | return new OctaneHandler; 43 | } 44 | 45 | // If not, we try to get the handler from the Laravel container 46 | return $this->laravel()->get($id); 47 | } 48 | 49 | public function has(string $id): bool 50 | { 51 | // By default, we check if the handler is a file name (classic Bref behavior) 52 | if ($this->fileLocator->has($id)) { 53 | return true; 54 | } 55 | 56 | // The Octane handler is special: it is not created by Laravel's container 57 | if ($id === OctaneHandler::class) { 58 | return true; 59 | } 60 | 61 | // If not, we try to get the handler from the Laravel container 62 | return $this->laravel()->has($id); 63 | } 64 | 65 | /** 66 | * Create and return the Laravel application. 67 | */ 68 | private function laravel(): Application 69 | { 70 | // Only create it once 71 | if ($this->laravel) { 72 | return $this->laravel; 73 | } 74 | 75 | $bootstrapFile = LaravelPathFinder::app(); 76 | 77 | $this->laravel = require $bootstrapFile; 78 | 79 | if (! $this->laravel instanceof Application) { 80 | throw new RuntimeException(sprintf( 81 | "Expected the `%s` file to return a %s object, instead it returned `%s`", 82 | $bootstrapFile, 83 | Application::class, 84 | is_object($this->laravel) ? get_class($this->laravel) : gettype($this->laravel), 85 | )); 86 | } 87 | 88 | $kernel = $this->laravel->make(Kernel::class); 89 | $kernel->bootstrap(); 90 | 91 | return $this->laravel; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Http/HttpHandler.php: -------------------------------------------------------------------------------- 1 | kernel->handle($request); 45 | } 46 | 47 | $this->kernel->terminate($request, $response); 48 | 49 | $response->prepare($request); 50 | 51 | return new HttpResponse( 52 | $response->getContent(), 53 | $response->headers->all(), 54 | $response->getStatusCode() 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Http/Middleware/ServeStaticAssets.php: -------------------------------------------------------------------------------- 1 | getPathInfo(), '/')); 23 | $file = public_path($requestPath); 24 | 25 | if (! in_array($requestPath, Config::get('bref.assets', [])) || ! file_exists($file)) { 26 | return $next($request); 27 | } 28 | 29 | return Response::make(file_get_contents($file), 200, [ 30 | 'Cache-Control' => 'public', 31 | 'Content-Type' => $this->getMimeType($file), 32 | 'Content-Length' => filesize($file), 33 | 'ETag' => hash_file('sha1', $file), 34 | ]); 35 | } 36 | 37 | /** 38 | * Returns the cleaned mime-type of the given file. 39 | * 40 | * @param string $file 41 | * @return string 42 | */ 43 | protected function getMimeType(string $file) 44 | { 45 | $mimeType = finfo_file(finfo_open(FILEINFO_MIME_TYPE), $file); 46 | 47 | if ($mimeType === 'image/vnd.microsoft.icon') { 48 | return 'image/x-icon'; 49 | } 50 | 51 | if ($mimeType !== 'text/plain') { 52 | return $mimeType; 53 | } 54 | 55 | return match (pathinfo($file, PATHINFO_EXTENSION)) { 56 | 'js' => 'text/javascript', 57 | 'css' => 'text/css', 58 | 'html' => 'text/html', 59 | default => 'text/plain', 60 | }; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Http/OctaneHandler.php: -------------------------------------------------------------------------------- 1 | octaneClient = new OctaneClient( 24 | $path ?? getcwd(), 25 | (bool) ($_ENV['OCTANE_PERSIST_DATABASE_SESSIONS'] ?? false) 26 | ); 27 | } 28 | 29 | /** 30 | * {@inheritDoc} 31 | */ 32 | public function handleRequest(HttpRequestEvent $event, Context $context): HttpResponse 33 | { 34 | $request = Request::createFromBase( 35 | SymfonyRequestBridge::convertRequest($event, $context) 36 | ); 37 | 38 | if (MaintenanceMode::active()) { 39 | $response = MaintenanceMode::response($request)->prepare($request); 40 | } else { 41 | $response = $this->octaneClient->handle($request); 42 | } 43 | 44 | if (! $response->headers->has('Content-Type')) { 45 | $response->prepare($request); // https://github.com/laravel/framework/pull/43895 46 | } 47 | 48 | $content = $response instanceof BinaryFileResponse 49 | ? $response->getFile()->getContent() 50 | : $response->getContent(); 51 | 52 | return new HttpResponse( 53 | $content, 54 | $response->headers->all(), 55 | $response->getStatusCode() 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Http/SymfonyRequestBridge.php: -------------------------------------------------------------------------------- 1 | createRequest($psr7Request); 26 | 27 | $symfonyRequest->server->add([ 28 | 'HTTP_X_REQUEST_ID' => $context->getAwsRequestId(), 29 | 'LAMBDA_INVOCATION_CONTEXT' => json_encode($context), 30 | 'LAMBDA_REQUEST_CONTEXT' => json_encode($event->getRequestContext()), 31 | ]); 32 | 33 | return $symfonyRequest; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/LaravelPathFinder.php: -------------------------------------------------------------------------------- 1 | wantsJson()) { 47 | $file = file_exists($_ENV['LAMBDA_TASK_ROOT'] . '/php/503.json') 48 | ? $_ENV['LAMBDA_TASK_ROOT'] . '/php/503.json' 49 | : realpath(__DIR__ . '/../stubs/503.json'); 50 | 51 | return JsonResponse::fromJsonString(file_get_contents($file), 503); 52 | } 53 | 54 | $file = file_exists($_ENV['LAMBDA_TASK_ROOT'] . '/php/503.html') 55 | ? $_ENV['LAMBDA_TASK_ROOT'] . '/php/503.html' 56 | : realpath(__DIR__ . '/../stubs/503.html'); 57 | 58 | return new Response(file_get_contents($file), 503); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Octane/OctaneClient.php: -------------------------------------------------------------------------------- 1 | worker = tap( 34 | new Worker(new ApplicationFactory($basePath), $this) 35 | )->boot()->onRequestHandled( 36 | static::manageDatabaseSessions($persistDatabaseSession) 37 | ); 38 | } 39 | 40 | /** 41 | * Handle the given request. 42 | * 43 | * @param \Illuminate\Http\Request $request 44 | * @return \Symfony\Component\HttpFoundation\Response 45 | */ 46 | public function handle(Request $request): Response 47 | { 48 | $this->worker->application()->useStoragePath('/tmp/storage'); 49 | 50 | $this->worker->handle($request, new RequestContext); 51 | 52 | $response = clone $this->response->response; 53 | $this->response = null; 54 | 55 | return $response; 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | */ 61 | public function error(Throwable $exception, Application $app, Request $request, RequestContext $context): void 62 | { 63 | try { 64 | $this->response = new OctaneResponse( 65 | $app[ExceptionHandler::class]->render($request, $exception) 66 | ); 67 | } catch (Throwable $throwable) { 68 | fwrite(STDERR, $throwable->getMessage()); 69 | fwrite(STDERR, $exception->getMessage()); 70 | 71 | $this->response = new OctaneResponse( 72 | new Response('Internal Server Error', 500) 73 | ); 74 | } 75 | } 76 | 77 | /** 78 | * {@inheritdoc} 79 | */ 80 | public function respond(RequestContext $context, OctaneResponse $response): void 81 | { 82 | $this->response = $response; 83 | } 84 | 85 | /** 86 | * {@inheritdoc} 87 | */ 88 | public function marshalRequest(RequestContext $context): array 89 | { 90 | return []; 91 | } 92 | 93 | /** 94 | * Manage the database sessions. 95 | * 96 | * @param bool $persistDatabaseSession 97 | * @return callable 98 | */ 99 | protected static function manageDatabaseSessions(bool $persistDatabaseSession) 100 | { 101 | return function ($request, $response, $sandbox) use ($persistDatabaseSession) { 102 | if ($persistDatabaseSession) { 103 | return; 104 | } 105 | 106 | if (! $sandbox->resolved('db')) { 107 | return; 108 | } 109 | 110 | collect($sandbox->make('db')->getConnections())->each->disconnect(); 111 | }; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Queue/QueueHandler.php: -------------------------------------------------------------------------------- 1 | make(QueueManager::class) 41 | ->connection($connection); 42 | 43 | if (! $queue instanceof SqsQueue) { 44 | throw new RuntimeException('Default queue connection is not a SQS connection'); 45 | } 46 | 47 | $this->sqs = $queue->getSqs(); 48 | } 49 | 50 | /** 51 | * Handle SQS event. 52 | */ 53 | public function handleSqs(SqsEvent $event, Context $context): void 54 | { 55 | /** @var Worker $worker */ 56 | $worker = $this->container->makeWith(Worker::class, [ 57 | 'isDownForMaintenance' => fn () => MaintenanceMode::active(), 58 | 'resetScope' => fn() => $this->resetLaravel(), 59 | ]); 60 | 61 | $worker->setCache( 62 | $this->container->make(Cache::class) 63 | ); 64 | 65 | foreach ($event->getRecords() as $sqsRecord) { 66 | $timeout = $this->calculateJobTimeout($context->getRemainingTimeInMillis()); 67 | 68 | $worker->runSqsJob( 69 | $job = $this->marshalJob($sqsRecord), 70 | $this->connection, 71 | $this->gatherWorkerOptions($timeout), 72 | ); 73 | 74 | if (! $job->hasFailed() && ! $job->isDeleted()) { 75 | $job->delete(); 76 | } 77 | } 78 | } 79 | 80 | /** 81 | * Marshal the job with the given Bref SQS record. 82 | * 83 | * @param \Bref\Event\Sqs\SqsRecord $sqsRecord 84 | * @return \Bref\LaravelBridge\Queue\SqsJob 85 | */ 86 | protected function marshalJob(SqsRecord $sqsRecord): SqsJob 87 | { 88 | $message = [ 89 | 'MessageId' => $sqsRecord->getMessageId(), 90 | 'ReceiptHandle' => $sqsRecord->getReceiptHandle(), 91 | 'Body' => $sqsRecord->getBody(), 92 | 'Attributes' => $sqsRecord->toArray()['attributes'], 93 | 'MessageAttributes' => $sqsRecord->getMessageAttributes(), 94 | ]; 95 | 96 | return new SqsJob( 97 | $this->container, 98 | $this->sqs, 99 | $message, 100 | $this->connection, 101 | $sqsRecord->getQueueName(), 102 | ); 103 | } 104 | 105 | /** 106 | * Gather all of the queue worker options as a single object. 107 | * 108 | * @param int $timeout 109 | * @return \Illuminate\Queue\WorkerOptions 110 | */ 111 | protected function gatherWorkerOptions(int $timeout): WorkerOptions 112 | { 113 | $options = [ 114 | 0, // backoff 115 | 512, // memory 116 | $timeout, // timeout 117 | 0, // sleep 118 | 3, // maxTries 119 | false, // force 120 | false, // stopWhenEmpty 121 | 0, // maxJobs 122 | 0, // maxTime 123 | ]; 124 | 125 | if (property_exists(WorkerOptions::class, 'name')) { 126 | $options = array_merge(['default'], $options); 127 | } 128 | 129 | return new WorkerOptions(...$options); 130 | } 131 | 132 | /** 133 | * Calculate the timeout for a job 134 | * 135 | * @param int $remainingInvocationTimeInMs 136 | * @return int 137 | */ 138 | protected function calculateJobTimeout(int $remainingInvocationTimeInMs): int 139 | { 140 | return max((int) (($remainingInvocationTimeInMs - self::JOB_TIMEOUT_SAFETY_MARGIN) / 1000), 0); 141 | } 142 | 143 | /** 144 | * Called on each new job to reset Laravel between jobs. 145 | */ 146 | private function resetLaravel(): void 147 | { 148 | if (method_exists($this->container['log'], 'flushSharedContext')) { 149 | $this->container['log']->flushSharedContext(); 150 | } 151 | 152 | if (method_exists($this->container['log'], 'withoutContext')) { 153 | $this->container['log']->withoutContext(); 154 | } 155 | 156 | if (method_exists($this->container['db'], 'getConnections')) { 157 | foreach ($this->container['db']->getConnections() as $connection) { 158 | $connection->resetTotalQueryDuration(); 159 | $connection->allowQueryDurationHandlersToRunAgain(); 160 | } 161 | } 162 | 163 | $this->container->forgetScopedInstances(); 164 | 165 | Facade::clearResolvedInstances(); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/Queue/SqsJob.php: -------------------------------------------------------------------------------- 1 | released = true; 16 | 17 | $payload = $this->payload(); 18 | $payload['attempts'] = ($payload['attempts'] ?? 0) + 1; 19 | 20 | $this->sqs->deleteMessage([ 21 | 'QueueUrl' => $this->queue, 22 | 'ReceiptHandle' => $this->job['ReceiptHandle'], 23 | ]); 24 | 25 | $sqsMessage = [ 26 | 'QueueUrl' => $this->queue, 27 | 'MessageBody' => json_encode($payload), 28 | 'DelaySeconds' => $this->secondsUntil($delay) 29 | ]; 30 | 31 | if (Str::endsWith($this->queue, '.fifo')) { 32 | $sqsMessage['MessageGroupId'] = $this->job['Attributes']['MessageGroupId']; 33 | $sqsMessage['MessageDeduplicationId'] = $this->parseDeduplicationId($payload['attempts']); 34 | unset($sqsMessage["DelaySeconds"]); 35 | } 36 | 37 | $this->sqs->sendMessage($sqsMessage); 38 | } 39 | 40 | /** 41 | * {@inheritDoc} 42 | */ 43 | public function attempts() 44 | { 45 | return ($this->payload()['attempts'] ?? 0) + 1; 46 | } 47 | 48 | /** 49 | * Create new MessageDeduplicationId 50 | * appending attempt at the end so the message will not be ignored 51 | * 52 | * https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_SendMessage.html#API_SendMessage_RequestSyntax 53 | */ 54 | private function parseDeduplicationId($attempts) 55 | { 56 | return $this->job['Attributes']['MessageDeduplicationId'] . '-' . $attempts; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Queue/Worker.php: -------------------------------------------------------------------------------- 1 | resetScope)) { 16 | ($this->resetScope)(); 17 | } 18 | 19 | pcntl_async_signals(true); 20 | 21 | pcntl_signal(SIGALRM, function () use ($job) { 22 | $this->markJobAsFailedIfItShouldFailOnTimeout( 23 | $job->getConnectionName(), 24 | $job, 25 | $this->maxAttemptsExceededException($job), 26 | ); 27 | 28 | // exit so that PHP will shutdown and close DB connections etc. 29 | exit(1); 30 | }); 31 | 32 | pcntl_alarm( 33 | max($this->timeoutForJob($job, $options), 0) 34 | ); 35 | 36 | $this->runJob($job, $connectionName, $options); 37 | 38 | pcntl_alarm(0); // cancel the previous alarm 39 | } 40 | 41 | /** 42 | * Mark the given job as failed if it should fail on timeouts. 43 | * 44 | * @param string $connectionName 45 | * @param \Illuminate\Contracts\Queue\Job $job 46 | * @param \Throwable $e 47 | * @return void 48 | */ 49 | protected function markJobAsFailedIfItShouldFailOnTimeout($connectionName, $job, Throwable $e) 50 | { 51 | if (method_exists($job, 'shouldFailOnTimeout') ? $job->shouldFailOnTimeout() : true) { 52 | $this->failJob($job, $e); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/StorageDirectories.php: -------------------------------------------------------------------------------- 1 | ! is_dir($directory)); 33 | 34 | if (count($directories) && defined('STDERR') && !getenv('BREF_LARAVEL_OMIT_INITLOG')) { 35 | fwrite(STDERR, 'Creating storage directories: ' . implode(', ', $directories) . PHP_EOL); 36 | } 37 | 38 | foreach ($directories as $directory) { 39 | if (! mkdir($directory, 0755, true) && ! is_dir($directory)) { 40 | throw new RuntimeException("Directory {$directory} could not be created"); 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/bref-init.php: -------------------------------------------------------------------------------- 1 | /dev/null'; 46 | if (!getenv('BREF_LARAVEL_OMIT_INITLOG')) { 47 | fwrite(STDERR, "Running 'php artisan config:cache' to cache the Laravel configuration\n"); 48 | // 1>&2 redirects the output to STDERR to avoid messing up HTTP responses with FPM 49 | $outputDestination = '1>&2'; 50 | } 51 | 52 | passthru("php $laravelRoot/artisan config:cache {$outputDestination}"); 53 | } 54 | }); 55 | 56 | Bref::setContainer(static fn() => new HandlerResolver); 57 | -------------------------------------------------------------------------------- /stubs/503.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |We are currently down for maintenance.
31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /stubs/503.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": { 3 | "code": 503, 4 | "message": "Service Unavailable" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /stubs/serverless.yml: -------------------------------------------------------------------------------- 1 | service: laravel 2 | 3 | # Set your team ID if you are using Bref Cloud 4 | #bref: 5 | # team: my-team-id 6 | 7 | provider: 8 | name: aws 9 | # The AWS region in which to deploy (us-east-1 is the default) 10 | region: us-east-1 11 | # Environment variables 12 | environment: 13 | APP_ENV: production # Or use ${sls:stage} if you want the environment to match the stage 14 | SESSION_DRIVER: cookie # Change to database if you have set up a database 15 | 16 | package: 17 | # Files and directories to exclude from deployment 18 | patterns: 19 | - '!node_modules/**' 20 | - '!public/storage' 21 | - '!resources/assets/**' 22 | - '!resources/css/**' 23 | - '!resources/images/**' 24 | - '!resources/js/**' 25 | - '!storage/**' 26 | - '!tests/**' 27 | - '!database/*.sqlite' 28 | # Exclude assets except for the manifest file 29 | - '!public/build/**' 30 | - 'public/build/manifest.json' 31 | 32 | functions: 33 | 34 | # This function runs the Laravel website/API 35 | web: 36 | handler: public/index.php 37 | runtime: php-82-fpm 38 | timeout: 28 # in seconds (API Gateway has a timeout of 29 seconds) 39 | events: 40 | - httpApi: '*' 41 | 42 | # This function lets us run artisan commands in Lambda 43 | artisan: 44 | handler: artisan 45 | runtime: php-82-console 46 | timeout: 720 # in seconds 47 | # Uncomment to also run the scheduler every minute 48 | #events: 49 | # - schedule: 50 | # rate: rate(1 minute) 51 | # input: '"schedule:run"' 52 | 53 | plugins: 54 | # We need to include the Bref plugin 55 | - ./vendor/bref/bref 56 | --------------------------------------------------------------------------------