Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ app/**/temporal.exe
app/tests/**/*.log
app/runtime
app/vendor
lambda/.rie
lambda/rr
lambda/.rr-build
lambda/.rr-build-*
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ The following samples demonstrate some of the more complex aspects associated wi

- **[Interceptors](https://github.com/temporalio/samples-php/tree/master/app/src/Interceptors)**: Demonstrates how to use Workflow and Activity interceptors to implement custom logic.

- **[AWS Lambda](https://github.com/temporalio/samples-php/tree/master/app/src/Lambda)**: Demonstrates running a Worker as an AWS Lambda custom runtime, where a Workflow outlives a single invocation.

### Testing samples

- **[Feature](https://github.com/temporalio/samples-php/tree/master/app/tests/Feature)**: Demonstrates how to mock activities and test Workflows using temporal test server.
Expand Down
50 changes: 50 additions & 0 deletions app/src/Lambda/ExecuteCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

/**
* This file is part of Temporal package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Temporal\Samples\Lambda;

use Carbon\CarbonInterval;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Temporal\Client\WorkflowOptions;
use Temporal\SampleUtils\Command;

class ExecuteCommand extends Command
{
public const TASK_QUEUE = 'php-lambda';

protected const NAME = 'lambda';
protected const DESCRIPTION = 'Execute Lambda\GreetingWorkflow on a worker running in AWS Lambda';

public function execute(InputInterface $input, OutputInterface $output): int
{
$workflow = $this->workflowClient->newWorkflowStub(
GreetingWorkflowInterface::class,
WorkflowOptions::new()
->withTaskQueue(self::TASK_QUEUE)
->withWorkflowExecutionTimeout(CarbonInterval::minutes(5)),
);

$output->writeln('Starting <comment>GreetingWorkflow</comment> on the <comment>' . self::TASK_QUEUE . '</comment> task queue... ');

$run = $this->workflowClient->start($workflow, 'Antony');

$output->writeln(
\sprintf('Started: WorkflowID=<fg=magenta>%s</fg=magenta>', $run->getExecution()->getID()),
);
$output->writeln('The workflow sleeps for 30 seconds, so it outlives a single Lambda invocation.');
$output->writeln('Keep invoking the function until it completes.');

$output->writeln(\sprintf("Result:\n<info>%s</info>", $run->getResult()));

return self::SUCCESS;
}
}
20 changes: 20 additions & 0 deletions app/src/Lambda/GreetingActivity.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

/**
* This file is part of Temporal package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Temporal\Samples\Lambda;

class GreetingActivity implements GreetingActivityInterface
{
public function compose(string $greeting, string $name): string
{
return \sprintf('%s, %s!', $greeting, $name);
}
}
22 changes: 22 additions & 0 deletions app/src/Lambda/GreetingActivityInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

/**
* This file is part of Temporal package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Temporal\Samples\Lambda;

use Temporal\Activity\ActivityInterface;
use Temporal\Activity\ActivityMethod;

#[ActivityInterface(prefix: 'LambdaGreeting.')]
interface GreetingActivityInterface
{
#[ActivityMethod(name: 'compose')]
public function compose(string $greeting, string $name): string;
}
38 changes: 38 additions & 0 deletions app/src/Lambda/GreetingWorkflow.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

/**
* This file is part of Temporal package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Temporal\Samples\Lambda;

use Carbon\CarbonInterval;
use Temporal\Activity\ActivityOptions;
use Temporal\Workflow;

class GreetingWorkflow implements GreetingWorkflowInterface
{
private $activity;

public function __construct()
{
$this->activity = Workflow::newActivityStub(
GreetingActivityInterface::class,
ActivityOptions::new()->withStartToCloseTimeout(CarbonInterval::seconds(10)),
);
}

public function greet(string $name)
{
$greeting = yield $this->activity->compose('Hello', $name);

yield Workflow::timer(CarbonInterval::seconds(30));

return $greeting . ' (resumed after a 30 second timer)';
}
}
22 changes: 22 additions & 0 deletions app/src/Lambda/GreetingWorkflowInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

/**
* This file is part of Temporal package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Temporal\Samples\Lambda;

use Temporal\Workflow\WorkflowInterface;
use Temporal\Workflow\WorkflowMethod;

#[WorkflowInterface]
interface GreetingWorkflowInterface
{
#[WorkflowMethod(name: 'LambdaGreeting')]
public function greet(string $name);
}
136 changes: 136 additions & 0 deletions app/src/Lambda/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# AWS Lambda

A Temporal worker running as an AWS Lambda custom runtime.

Lambda has no place for a long-lived process: a container is alive only while it handles an
invocation. RoadRunner itself handles that, through its `lambda` plugin: the plugin serves the
Lambda Runtime API, and on every invocation it starts the Temporal workers, lets them poll until
the deadline minus a shutdown buffer, then drains and stops them so the runtime can still answer.
The PHP worker pools stay up for the whole lifetime of the execution environment, so no PHP
process is restarted between invocations. Workflow state lives on the server, not in the process,
so a workflow survives across invocations by replay.

The PHP side needs nothing Lambda-specific: `lambda/worker.php` is an ordinary worker built with
`WorkerFactory::create()`.

The sample workflow makes that visible: it runs an activity, then sleeps for 30 seconds. With the
default function timeout of 30 seconds one invocation polls for 23 of them, so it is not enough —
the first invocation runs the activity and leaves the workflow sleeping, the second one picks it
up and returns the result.

## Layout

| path | role |
|---|---|
| `app/src/Lambda/` | workflow, activity and the client command |
| `lambda/worker.php` | the worker the function runs, with Lambda-tuned `WorkerOptions` |
| `lambda/.rr.yaml` | RoadRunner config: the `lambda` section plus the usual `rpc:` and `temporal:` |
| `lambda/Dockerfile` | the image, entrypoint `rr serve` |
| `lambda/velox.toml` | the plugin list the RoadRunner binary is built from |
| `lambda/Makefile` | build the binary and the image, run under the Runtime Interface Emulator, invoke |

## Run it locally

**1. Start Temporal.** From the repository root:

```bash
docker compose up -d temporal
```

**2. Build the image and start the emulator.**

```bash
cd lambda && make run
```

`make run` builds the RoadRunner binary with [velox](https://github.com/roadrunner-server/velox)
from the plugin list in `lambda/velox.toml`, builds the image, downloads the AWS Lambda Runtime
Interface Emulator and starts the function on `http://localhost:9000`. Install velox once with
`go install github.com/roadrunner-server/velox/v3/cmd/vx@latest`. For an x86 function set
`arch = "amd64"` in `velox.toml` and pass `PLATFORM=linux/amd64`.

The binary carries only the plugins the worker needs, which is why it is 28MB rather than the
59MB of the official build. While the `lambda` plugin is unreleased, `velox.toml` points at a
local checkout through a `[[replaces]]` block; drop that block once it ships.

**3. Start the workflow.** In another terminal:

```bash
docker compose exec app php app.php lambda
```

It starts the workflow on the `php-lambda` task queue and waits for the result.

**4. Invoke the function.** Each call runs the worker for one invocation:

```bash
cd lambda && make invoke
```

Call it twice: the first invocation runs the activity, the second one resumes the workflow after
the timer and the client prints

```
Hello, Antony! (resumed after a 30 second timer)
```

`make logs` follows the worker output, `make stop` removes the container.

## Configuration

The plugin takes over only when `AWS_LAMBDA_RUNTIME_API` is present, so the same image behaves
like an ordinary worker outside Lambda. Its two options live in `lambda/.rr.yaml`:

| option | default | meaning |
|---|---|---|
| `lambda.shutdown_buffer` | `graceful_timeout + 1s` | time reserved before the deadline to drain the workers and answer the Runtime API |
| `lambda.graceful_timeout` | `5s` | how long the workers may drain in-flight tasks; also becomes the worker's `WorkerStopTimeout` unless `worker.php` sets one |

The buffer must exceed the graceful timeout, otherwise the plugin refuses to start. Set the Lambda
function timeout well above the buffer, or every invocation ends before the worker has polled
anything.

The worker's own variables — `TEMPORAL_ADDRESS`, `TEMPORAL_NAMESPACE` and `TEMPORAL_TASK_QUEUE` —
are read by `lambda/worker.php` and `lambda/.rr.yaml`.

## Deploy

Push the image to ECR and create the function from it. A container image needs no runtime
identifier — pick the architecture that matches the image instead; `make build` targets
`linux/arm64`, so the function has to be arm64 as well (pass `PLATFORM=linux/amd64 GOARCH=amd64`
for x86).

Nothing triggers the worker on its own: drive it with an EventBridge schedule, and size the
function timeout and the schedule interval to the latency you want from your task queue. The
function bills for the whole invocation, polling included, so the schedule is the knob that
trades cost against how quickly a task is picked up.

Give the function at least 512 MB. One invocation runs RoadRunner plus three PHP processes and
sits around 95 MB on top of the base image, and Lambda scales CPU with memory, so the smallest
sizes also make the start slower.

Reaching Temporal from Lambda is on you: a function with no VPC attached has internet access and
can dial Temporal Cloud directly, while a self-hosted server inside a VPC needs the function
attached to that VPC. Temporal Cloud also needs credentials, which this sample does not set up —
add a `tls` section with the client certificate and key, or an API key, to `lambda/.rr.yaml`.

## Worker deployment versioning

Temporal's Serverless Worker support expects versioned workers, so that a workflow stays on the
build it started on. Set `TEMPORAL_DEPLOYMENT_NAME` and `TEMPORAL_BUILD_ID` and the worker
registers with `VersioningBehavior::Pinned`:

```bash
cd lambda && make run TEMPORAL_DEPLOYMENT_NAME=php-lambda-worker TEMPORAL_BUILD_ID=v1
```

A versioned worker receives no tasks until its version is made current, so do that once per
build id:

```bash
temporal worker deployment set-current-version \
--deployment-name php-lambda-worker --build-id v1 --yes
```

Leaving `TEMPORAL_DEPLOYMENT_NAME` empty, as the sample does by default, runs the worker
unversioned.
33 changes: 33 additions & 0 deletions lambda/.rr.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
version: '3'

# Required: Activity::heartbeat() reaches RoadRunner over this socket.
rpc:
listen: tcp://127.0.0.1:6001

server:
command: "php worker.php"
relay: pipes

temporal:
address: "${TEMPORAL_ADDRESS}"
namespace: "${TEMPORAL_NAMESPACE}"
# A worker that polls for one invocation never fills the default cache of 10000.
cache_size: 30
activities:
num_workers: 2
allocate_timeout: 5s

# Served by the lambda plugin, which takes over only when
# AWS_LAMBDA_RUNTIME_API is present in the environment.
lambda:
# Reserved before the invocation deadline to stop the Temporal workers and
# answer the Runtime API. Must exceed graceful_timeout.
shutdown_buffer: 6s
# How long the workers may drain in-flight tasks. It also becomes the
# worker's WorkerStopTimeout unless worker.php sets one.
graceful_timeout: 5s

logs:
mode: production
encoding: json
level: info
44 changes: 44 additions & 0 deletions lambda/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
ARG PHP_VERSION=8.3-cli-alpine
ARG COMPOSER_VERSION=2.9.4
ARG PROTOBUF_VERSION=5.36.0

FROM composer:${COMPOSER_VERSION} AS composer


FROM php:${PHP_VERSION} AS extensions
ARG PROTOBUF_VERSION
RUN set -eux; \
apk add --no-cache --virtual .build-deps $PHPIZE_DEPS linux-headers; \
pecl install protobuf-${PROTOBUF_VERSION}; \
docker-php-ext-enable protobuf; \
docker-php-ext-install -j"$(nproc)" pcntl sockets; \
apk del .build-deps

FROM extensions AS vendor
COPY --from=composer /usr/bin/composer /usr/local/bin/composer
WORKDIR /var/task
COPY lambda/composer.json lambda/composer.lock ./
RUN composer install --no-dev --no-scripts --no-interaction --no-progress --prefer-dist
COPY app/src/Lambda/ ./src/
RUN composer dump-autoload --no-dev --classmap-authoritative

FROM php:${PHP_VERSION} AS runtime

COPY --from=extensions /usr/local/lib/php/extensions/ /usr/local/lib/php/extensions/
COPY --from=extensions /usr/local/etc/php/conf.d/ /usr/local/etc/php/conf.d/
COPY lambda/rr /usr/local/bin/rr
COPY lambda/php.ini /usr/local/etc/php/conf.d/zz-lambda.ini

# Lambda gives a writable /tmp only; keep every cache out of the read-only image.
ENV HOME=/tmp \
LAMBDA_TASK_ROOT=/var/task

WORKDIR /var/task
COPY --from=vendor /var/task/vendor/ ./vendor/
COPY app/src/Lambda/ ./src/
COPY lambda/worker.php lambda/.rr.yaml ./

RUN addgroup -g 10001 -S app && adduser -S -D -H -u 10001 -G app -h /tmp app
USER 10001:10001

ENTRYPOINT ["rr", "serve"]
Loading
Loading