RoadRunner can run PHP as AWS Lambda function.
PHP worker does not require any specific configuration to run inside a Lambda function. We can use the default snippet with internal counter to demonstrate how workers are being reused:
Copy <?php
/**
* @var Goridge\RelayInterface $relay
*/
use Spiral\Goridge;
use Spiral\RoadRunner;
ini_set('display_errors', 'stderr');
require __DIR__ . "/vendor/autoload.php";
$worker = RoadRunner\Worker::create();
$psr7 = new RoadRunner\Http\PSR7Worker(
$worker,
new \Nyholm\Psr7\Factory\Psr17Factory(),
new \Nyholm\Psr7\Factory\Psr17Factory(),
new \Nyholm\Psr7\Factory\Psr17Factory()
);
while ($req = $psr7->waitRequest()) {
try {
$resp = new \Nyholm\Psr7\Response();
$resp->getBody()->write("hello world");
$psr7->respond($resp);
} catch (\Throwable $e) {
$psr7->getWorker()->error((string)$e);
}
}
Copy composer require spiral/roadrunner-http nyholm/psr7
Copy package main
import (
_ "embed"
"log"
"log/slog"
"os"
"os/signal"
"sync"
"syscall"
"time"
"github.com/roadrunner-server/config/v4"
"github.com/roadrunner-server/endure/v2"
"github.com/roadrunner-server/logger/v4"
"github.com/roadrunner-server/server/v4"
)
//go:embed .rr.yaml
var rrYaml []byte
func main() {
_ = os.Setenv("PATH", os.Getenv("PATH")+":"+os.Getenv("LAMBDA_TASK_ROOT"))
_ = os.Setenv("LD_LIBRARY_PATH", "./lib:/lib64:/usr/lib64")
cont := endure.New(slog.LevelError)
cfg := &config.Plugin{
Version: "2024.1.0",
Timeout: time.Second * 30,
Prefix: "rr",
Type: "yaml",
ReadInCfg: rrYaml,
}
err := cont.RegisterAll(
cfg,
&logger.Plugin{},
&Plugin{},
&server.Plugin{},
)
if err != nil {
log.Fatal(err)
}
err = cont.Init()
if err != nil {
log.Fatal(err)
}
ch, err := cont.Serve()
if err != nil {
log.Fatal(err)
}
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
wg := &sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case e := <-ch:
err = cont.Stop()
if err != nil {
log.Println(e.Error.Error())
}
case <-sig:
err = cont.Stop()
if err != nil {
log.Println(err.Error())
}
return
}
}
}()
wg.Wait()
}
Copy package main
import (
"context"
"sync"
"time"
"github.com/goccy/go-json"
"github.com/roadrunner-server/errors"
"github.com/roadrunner-server/goridge/v3/pkg/frame"
"github.com/roadrunner-server/sdk/v4/pool"
"github.com/roadrunner-server/sdk/v4/worker"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/roadrunner-server/sdk/v4/payload"
poolImp "github.com/roadrunner-server/sdk/v4/pool/static_pool"
"go.uber.org/zap"
)
const (
pluginName string = "lambda"
)
type Plugin struct {
mu sync.Mutex
log *zap.Logger
srv Server
pldPool sync.Pool
wrkPool Pool
}
// Logger plugin
type Logger interface {
NamedLogger(name string) *zap.Logger
}
type Pool interface {
// Workers returns workers list associated with the pool.
Workers() (workers []*worker.Process)
// Exec payload
Exec(ctx context.Context, p *payload.Payload, stopCh chan struct{}) (chan *poolImp.PExec, error)
// RemoveWorker removes worker from the pool.
RemoveWorker(ctx context.Context) error
// AddWorker adds worker to the pool.
AddWorker() error
// Reset kill all workers inside the watcher and replaces with new
Reset(ctx context.Context) error
// Destroy all underlying stacks (but let them complete the task).
Destroy(ctx context.Context)
}
// Server creates workers for the application.
type Server interface {
NewPool(ctx context.Context, cfg *pool.Config, env map[string]string, _ *zap.Logger) (*poolImp.Pool, error)
}
func (p *Plugin) Init(srv Server, log Logger) error {
p.srv = srv
p.log = log.NamedLogger(pluginName)
p.pldPool = sync.Pool{
New: func() any {
return &payload.Payload{
Codec: frame.CodecJSON,
Context: make([]byte, 0, 100),
Body: make([]byte, 0, 100),
}
},
}
return nil
}
func (p *Plugin) Serve() chan error {
errCh := make(chan error, 1)
const op = errors.Op("plugin_serve")
p.mu.Lock()
defer p.mu.Unlock()
var err error
p.wrkPool, err = p.srv.NewPool(context.Background(), &pool.Config{
NumWorkers: 4,
AllocateTimeout: time.Second * 20,
DestroyTimeout: time.Second * 20,
}, nil, nil)
if err != nil {
errCh <- errors.E(op, err)
return errCh
}
go func() {
// register handler
lambda.Start(p.handler())
}()
return errCh
}
func (p *Plugin) Stop(ctx context.Context) error {
p.mu.Lock()
defer p.mu.Unlock()
if p.wrkPool != nil {
p.wrkPool.Destroy(ctx)
}
return nil
}
func (p *Plugin) handler() func(ctx context.Context, request events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) {
return func(ctx context.Context, request events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) {
requestJSON, err := json.Marshal(request)
if err != nil {
return events.APIGatewayV2HTTPResponse{Body: "", StatusCode: 500}, nil
}
ctxJSON, err := json.Marshal(ctx)
if err != nil {
return events.APIGatewayV2HTTPResponse{Body: "", StatusCode: 500}, nil
}
pld := p.getPld()
defer p.putPld(pld)
pld.Body = requestJSON
pld.Context = ctxJSON
re, err := p.wrkPool.Exec(ctx, pld, nil)
if err != nil {
return events.APIGatewayV2HTTPResponse{Body: "", StatusCode: 500}, nil
}
var r *payload.Payload
select {
case pl := <-re:
if pl.Error() != nil {
return events.APIGatewayV2HTTPResponse{Body: "", StatusCode: 500}, nil
}
// streaming is not supported
if pl.Payload().Flags&frame.STREAM != 0 {
return events.APIGatewayV2HTTPResponse{Body: "streaming is not supported", StatusCode: 500}, nil
}
// assign the payload
r = pl.Payload()
default:
return events.APIGatewayV2HTTPResponse{Body: "worker empty response", StatusCode: 500}, nil
}
var response events.APIGatewayV2HTTPResponse
err = json.Unmarshal(r.Body, &response)
if err != nil {
return events.APIGatewayV2HTTPResponse{Body: "", StatusCode: 500}, nil
}
return response, nil
}
}
func (p *Plugin) putPld(pld *payload.Payload) {
pld.Body = nil
pld.Context = nil
p.pldPool.Put(pld)
}
func (p *Plugin) getPld() *payload.Payload {
pld := p.pldPool.Get().(*payload.Payload)
return pld
}
Copy version: "3"
server:
command: "php handler.php"
relay: pipes
logs:
mode: production
level: error
encoding: json
output: [ stderr ]
endure:
grace_period: 1s
Here you can use full advantage of the RoadRunner, you can include any plugin here and configure it with the embedded config (within reasonable limits).
Copy CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags "-s" -o bootstrap-amd64 main.go plugin.go
zip main.zip * -r
You can now upload and invoke your handler using simple string event.
Repository with the full example