From f3fd052cf055d3477de3d365dbf12312beed1a9e Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Mon, 4 Aug 2025 23:18:25 +0200 Subject: [PATCH 01/19] initial commit Signed-off-by: Robert Landers --- cli/Makefile | 38 +-- cli/auth/keys.go | 4 +- cli/auth/keys_test.go | 4 +- cli/auth/resource.go | 6 +- cli/auth/resourceManager.go | 6 +- cli/auth/resource_test.go | 2 +- cli/auth/user.go | 2 +- cli/cli.go | 14 +- cli/export.go | 5 + cli/ext/ext.go | 437 +++++++++++++++++++++++++++++++++++ cli/ext/helpers/helpers.go | 100 ++++++++ cli/glue/glue.go | 4 +- cli/glue/response_writer.go | 4 +- cli/glue/state.go | 2 +- cli/go.mod | 8 +- cli/go.sum | 12 +- cli/lib/api.go | 8 +- cli/lib/billing.go | 6 +- cli/lib/consumer.go | 10 +- cli/lib/index.go | 2 +- cli/lib/indexer.go | 6 +- cli/lib/locks.go | 2 +- cli/test.http | 1 + cli/test.php | 10 + frankenphp-ext/ext/client.go | 34 +++ 25 files changed, 662 insertions(+), 65 deletions(-) create mode 100644 cli/export.go create mode 100644 cli/ext/ext.go create mode 100644 cli/ext/helpers/helpers.go create mode 100644 cli/test.http create mode 100644 cli/test.php create mode 100644 frankenphp-ext/ext/client.go diff --git a/cli/Makefile b/cli/Makefile index 1b938225..40cb3f3a 100644 --- a/cli/Makefile +++ b/cli/Makefile @@ -1,17 +1,25 @@ -TARGET := dphp-linux-* -BIN_PATH := ../bin -DOCKER_IMAGE := builder -DOCKER_TARGET := cli-base-alpine -BUILD_PATH := /go/src/app/cli/dist +# Variables +PHP_INCLUDES := $(shell php-config --includes) +PHP_LDFLAGS := $(shell php-config --ldflags) +PHP_LIBS := $(shell php-config --libs) -${BIN_PATH}/${TARGET}: cli.go */* go.mod build.sh build-php.sh ../Dockerfile - mkdir -p ${BIN_PATH} - cd .. && docker buildx build --pull --load --target ${DOCKER_TARGET} -t ${DOCKER_IMAGE} . - docker create --name ${DOCKER_IMAGE} ${DOCKER_IMAGE} || ( docker rm -f ${DOCKER_IMAGE} && false ) - docker cp ${DOCKER_IMAGE}:${BUILD_PATH}/dphp ${BIN_PATH}/ || ( docker rm -f ${DOCKER_IMAGE} && false ) - docker rm -f ${DOCKER_IMAGE} - upx -9 --force-pie ../bin/dphp-* +XCADDY_FLAGS := -ldflags='-w -s' -tags=nobadger,nomysql,nopgx,nodphp,nobrotli -../dist: ${BIN_PATH}/${TARGET} - docker create --name builder builder - docker cp ${DOCKER_IMAGE}:${BUILD_PATH} ../dist +LOCAL_MODULE := /home/withinboredom/code/durable-php/cli + +# Targets +frankenphp: ext/build/ext.go + CGO_ENABLED=1 \ + XCADDY_GO_BUILD_FLAGS="$(XCADDY_FLAGS)" \ + CGO_CFLAGS="$(PHP_INCLUDES)" \ + CGO_LDFLAGS="$(PHP_LDFLAGS) $(PHP_LIBS)" \ + xcaddy build \ + --output frankenphp \ + --with github.com/dunglas/frankenphp/caddy \ + --with github.com/bottledcode/durable-php/cli=$(LOCAL_MODULE) + +ext/build/ext.go: ext/ext.go + GEN_STUB_SCRIPT=/home/withinboredom/code/php-src/build/gen_stub.php \ + frankenphp extension-init ext/ext.go + +.PHONY: frankenphp diff --git a/cli/auth/keys.go b/cli/auth/keys.go index 83445308..ff00e33e 100644 --- a/cli/auth/keys.go +++ b/cli/auth/keys.go @@ -2,11 +2,11 @@ package auth import ( "context" - "durable_php/appcontext" - "durable_php/config" "encoding/base64" "errors" "fmt" + "github.com/bottledcode/durable-php/cli/appcontext" + "github.com/bottledcode/durable-php/cli/config" "github.com/golang-jwt/jwt/v4" "net/http" "strings" diff --git a/cli/auth/keys_test.go b/cli/auth/keys_test.go index abb281de..14658bf4 100644 --- a/cli/auth/keys_test.go +++ b/cli/auth/keys_test.go @@ -2,8 +2,8 @@ package auth import ( "context" - "durable_php/appcontext" - "durable_php/config" + "github.com/bottledcode/durable-php/cli/appcontext" + "github.com/bottledcode/durable-php/cli/config" "testing" ) diff --git a/cli/auth/resource.go b/cli/auth/resource.go index be0b7192..b772a437 100644 --- a/cli/auth/resource.go +++ b/cli/auth/resource.go @@ -2,12 +2,12 @@ package auth import ( "context" - "durable_php/appcontext" - "durable_php/glue" - "durable_php/ids" "encoding/json" "errors" "fmt" + "github.com/bottledcode/durable-php/cli/appcontext" + "github.com/bottledcode/durable-php/cli/glue" + "github.com/bottledcode/durable-php/cli/ids" "github.com/nats-io/nats.go/jetstream" "go.uber.org/zap" "net/http" diff --git a/cli/auth/resourceManager.go b/cli/auth/resourceManager.go index 27aadab5..3c2a31b2 100644 --- a/cli/auth/resourceManager.go +++ b/cli/auth/resourceManager.go @@ -2,10 +2,10 @@ package auth import ( "context" - "durable_php/appcontext" - "durable_php/glue" - "durable_php/ids" "encoding/json" + "github.com/bottledcode/durable-php/cli/appcontext" + "github.com/bottledcode/durable-php/cli/glue" + "github.com/bottledcode/durable-php/cli/ids" "github.com/modern-go/concurrent" "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" diff --git a/cli/auth/resource_test.go b/cli/auth/resource_test.go index bf0b85b1..83869c51 100644 --- a/cli/auth/resource_test.go +++ b/cli/auth/resource_test.go @@ -2,8 +2,8 @@ package auth import ( "context" - "durable_php/appcontext" "errors" + "github.com/bottledcode/durable-php/cli/appcontext" "github.com/stretchr/testify/assert" "testing" "time" diff --git a/cli/auth/user.go b/cli/auth/user.go index e2e5913f..0951ae93 100644 --- a/cli/auth/user.go +++ b/cli/auth/user.go @@ -2,7 +2,7 @@ package auth import ( "context" - "durable_php/appcontext" + "github.com/bottledcode/durable-php/cli/appcontext" "slices" ) diff --git a/cli/cli.go b/cli/cli.go index 53017760..157fd77c 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -1,3 +1,5 @@ +//go:build !nodphp + /* * Copyright ©2024 Robert Landers * @@ -24,14 +26,14 @@ package main import ( "context" - "durable_php/auth" - "durable_php/config" - "durable_php/glue" - "durable_php/ids" - di "durable_php/init" - "durable_php/lib" "encoding/json" "fmt" + "github.com/bottledcode/durable-php/cli/auth" + "github.com/bottledcode/durable-php/cli/config" + "github.com/bottledcode/durable-php/cli/glue" + "github.com/bottledcode/durable-php/cli/ids" + di "github.com/bottledcode/durable-php/cli/init" + "github.com/bottledcode/durable-php/cli/lib" "github.com/dunglas/frankenphp" "github.com/nats-io/nats-server/v2/server" "github.com/nats-io/nats-server/v2/test" diff --git a/cli/export.go b/cli/export.go new file mode 100644 index 00000000..2cd3d702 --- /dev/null +++ b/cli/export.go @@ -0,0 +1,5 @@ +//go:build nodphp + +package cli + +import _ "github.com/bottledcode/durable-php/cli/ext/build" diff --git a/cli/ext/ext.go b/cli/ext/ext.go new file mode 100644 index 00000000..32ebe3b0 --- /dev/null +++ b/cli/ext/ext.go @@ -0,0 +1,437 @@ +package ext + +import "C" + +import ( + "context" + "encoding/json" + "errors" + "github.com/bottledcode/durable-php/cli/auth" + "github.com/bottledcode/durable-php/cli/config" + "github.com/bottledcode/durable-php/cli/ext/helpers" + "github.com/bottledcode/durable-php/cli/glue" + "github.com/bottledcode/durable-php/cli/ids" + "github.com/bottledcode/durable-php/cli/lib" + "github.com/dunglas/frankenphp" + "github.com/nats-io/nats-server/v2/server" + "github.com/nats-io/nats-server/v2/test" + "github.com/nats-io/nats.go" + "github.com/nats-io/nats.go/jetstream" + "go.uber.org/zap" + "os" + "strings" + "sync" + "time" + "unsafe" +) + +/** +This is a php extension that operates as a client for durable php +*/ + +// export_php:namespace Bottledcode\DurablePhp\Ext + +func go_shutdown_module() { + os.RemoveAll(helpers.NatsState) +} + +// export_php:module init +func go_init_module() { + cfg, err := config.GetProjectConfig() + if err != nil { + panic(err) + } + helpers.Config = cfg + + helpers.Logger = helpers.GetLogger(zap.DebugLevel) + logger := helpers.Logger + + logger.Info("Starting Durable PHP") + + boostrapNats := cfg.Nat.Bootstrap + if cfg.Nat.Internal { + logger.Warn("Running in dev mode, all data will be deleted at the end of this") + helpers.NatsState, err = os.MkdirTemp("", "nats-state-*") + if err != nil { + panic(err) + } + + s := test.RunServer(&server.Options{ + Host: "localhost", + Port: 4222, + NoLog: true, + NoSigs: true, + JetStream: true, + MaxControlLine: 2048, + StoreDir: helpers.NatsState, + HTTPPort: 8222, + }) + defer s.Shutdown() + boostrapNats = true + } + + nopts := []nats.Option{ + nats.Compression(true), + nats.RetryOnFailedConnect(true), + } + + if cfg.Nat.Jwt != "" && cfg.Nat.Nkey != "" { + nopts = append(nopts, nats.UserCredentials(cfg.Nat.Jwt, cfg.Nat.Nkey)) + } + + if cfg.Nat.Tls.Ca != "" { + nopts = append(nopts, nats.RootCAs(strings.Split(cfg.Nat.Tls.Ca, ",")...)) + } + + if cfg.Nat.Tls.KeyFile != "" { + nopts = append(nopts, nats.ClientCert(cfg.Nat.Tls.ClientCert, cfg.Nat.Tls.KeyFile)) + } + + ns, err := nats.Connect(cfg.Nat.Url, nopts...) + if err != nil { + panic(err) + } + helpers.Js, err = jetstream.New(ns) + if err != nil { + panic(err) + } + ctx := context.WithValue(context.Background(), "bootstrap", cfg.Bootstrap) + + if boostrapNats { + stream, _ := helpers.Js.CreateStream(ctx, jetstream.StreamConfig{ + Name: cfg.Stream, + Description: "Handles durable-php events", + Subjects: []string{cfg.Stream + ".>"}, + Retention: jetstream.WorkQueuePolicy, + Storage: jetstream.FileStorage, + AllowRollup: false, + DenyDelete: true, + DenyPurge: true, + }) + _, _ = helpers.Js.CreateStream(ctx, jetstream.StreamConfig{ + Name: cfg.Stream + "_history", + Description: "The history of the stream", + Mirror: &jetstream.StreamSource{ + Name: cfg.Stream, + }, + Retention: jetstream.LimitsPolicy, + AllowRollup: true, + MaxAge: 7 * 24 * time.Hour, + Discard: jetstream.DiscardOld, + }) + + consumers := []string{ + string(ids.Activity), + string(ids.Entity), + string(ids.Orchestration), + } + + for _, kind := range consumers { + _, _ = stream.CreateConsumer(ctx, jetstream.ConsumerConfig{ + Durable: cfg.Stream + "-" + kind, + FilterSubject: cfg.Stream + "." + kind + ".>", + AckPolicy: jetstream.AckExplicitPolicy, + AckWait: 5 * time.Minute, + }) + } + } + + if len(cfg.Extensions.Search.Collections) > 0 { + for _, collection := range cfg.Extensions.Search.Collections { + switch collection { + case "entities": + err := lib.IndexerListen(ctx, cfg, ids.Entity, helpers.Js, logger) + if err != nil { + cfg.Extensions.Search.Collections = []string{} + logger.Warn("Disabling search extension due to failing to connect to typesense") + } + case "orchestrations": + err := lib.IndexerListen(ctx, cfg, ids.Orchestration, helpers.Js, logger) + if err != nil { + cfg.Extensions.Search.Collections = []string{} + logger.Warn("Disabling search extension due to failing to connect to typesense") + } + } + } + } + + if cfg.Extensions.Billing.Enabled { + if cfg.Extensions.Billing.Listen { + + billings := sync.Map{} + billings.Store("e", 0) + billings.Store("o", 0) + billings.Store("a", 0*time.Minute) + billings.Store("ac", 0) + + var incrementInt func(key string, amount int) + incrementInt = func(key string, amount int) { + var old interface{} + old, _ = billings.Load(key) + if !billings.CompareAndSwap(key, old, old.(int)+1) { + incrementInt(key, amount) + } + } + + var incrementDur func(key string, amount time.Duration) + incrementDur = func(key string, amount time.Duration) { + var old interface{} + old, _ = billings.Load(key) + if !billings.CompareAndSwap(key, old, old.(time.Duration)+amount) { + incrementDur(key, amount) + } + } + + /* + outputBillingStatus := func() { + costC := func(num interface{}, basis int) float64 { + return float64(num.(int)) * float64(basis) / 10_000_000 + } + + costA := func(dur interface{}, basis int) float64 { + duration := dur.(time.Duration) + seconds := duration.Seconds() + return float64(basis) * seconds / 100_000 + } + + avg := func(dur interface{}, count interface{}) time.Duration { + seconds := dur.(time.Duration).Seconds() + return time.Duration(seconds/float64(count.(int))) * time.Second + } + + e, _ := billings.Load("e") + o, _ := billings.Load("o") + ac, _ := billings.Load("ac") + a, _ := billings.Load("a") + + ecost := costC(e, cfg.Extensions.Billing.Costs.Entities.Cost) + ocost := costC(o, cfg.Extensions.Billing.Costs.Orchestrations.Cost) + acost := costA(a, cfg.Extensions.Billing.Costs.Activities.Cost) + + logger.Warn("Billing estimate", + zap.Any("launched entities", e), + zap.String("entity cost", fmt.Sprintf("$%.2f", ecost)), + zap.Any("launched orchestrations", o), + zap.String("orchestration cost", fmt.Sprintf("$%.2f", ocost)), + zap.Any("activity time", a), + zap.Any("activities launced", ac), + zap.Any("average activity time", avg(a, ac)), + zap.String("activity cost", fmt.Sprintf("$%.2f", acost)), + zap.String("total estimate", fmt.Sprintf("$%.2f", ecost+ocost+acost)), + ) + } + + go func() { + ticker := time.NewTicker(3 * time.Second) + for range ticker.C { + outputBillingStatus() + } + }() + */ + + billingStream, err := helpers.Js.CreateOrUpdateStream(ctx, jetstream.StreamConfig{ + Name: "billing", + Subjects: []string{ + "billing." + cfg.Stream + ".>", + }, + Storage: jetstream.FileStorage, + Retention: jetstream.LimitsPolicy, + MaxAge: 7 * 24 * time.Hour, + }) + if err != nil { + panic(err) + } + + entityConsumer, err := billingStream.CreateOrUpdateConsumer(ctx, jetstream.ConsumerConfig{ + Durable: "entityAggregator", + FilterSubjects: []string{ + "billing." + cfg.Stream + ".entities.>", + }, + }) + if err != nil { + panic(err) + } + + consume, err := entityConsumer.Consume(func(msg jetstream.Msg) { + incrementInt("e", 1) + msg.Ack() + }) + if err != nil { + panic(err) + } + defer consume.Drain() + + orchestrationConsumer, err := billingStream.CreateOrUpdateConsumer(ctx, jetstream.ConsumerConfig{ + Durable: "orchestrationAggregator", + FilterSubject: "billing." + cfg.Stream + ".orchestrations.>", + }) + if err != nil { + panic(err) + } + + consume, err = orchestrationConsumer.Consume(func(msg jetstream.Msg) { + incrementInt("o", 1) + msg.Ack() + }) + if err != nil { + panic(err) + } + defer consume.Drain() + + activityConsumer, err := billingStream.CreateOrUpdateConsumer(ctx, jetstream.ConsumerConfig{ + Durable: "activityAggregator", + FilterSubject: "billing." + cfg.Stream + ".activities.>", + }) + if err != nil { + panic(err) + } + + consume, err = activityConsumer.Consume(func(msg jetstream.Msg) { + incrementInt("ac", 1) + var ev lib.BillingEvent + err := json.Unmarshal(msg.Data(), &ev) + if err != nil { + panic(err) + } + incrementDur("a", ev.Duration) + msg.Ack() + }) + if err != nil { + panic(err) + } + defer consume.Drain() + } + + err := lib.StartBillingProcessor(ctx, cfg, helpers.Js, logger) + if err != nil { + panic(err) + } + } +} + +// Authorize the user to access the given event destination. +// (false, nil) means not found, while (true, nil) means they're allowed. +// Any error means the user is not authorised. +func Authorize(ctx context.Context, ev *glue.EventMessage, from *ids.StateId, preventCreation bool, operation auth.Operation) (bool, error) { + if !helpers.Config.Extensions.Authz.Enabled { + return true, nil + } + + rm := auth.GetResourceManager(ctx, helpers.Js) + r, err := rm.DiscoverResource(ctx, ids.ParseStateId(ev.Destination), from, helpers.Logger, preventCreation) + if err != nil { + err = errors.Join(errors.New("user is not authorised"), err) + helpers.ThrowPHPException(err.Error()) + return false, err + } + if r == nil { + return false, nil + } + + if !r.WantTo(operation, ctx) { + err = errors.New("user is not authorised") + helpers.ThrowPHPException(err.Error()) + return false, err + } + + return true, nil +} + +// export_php:function get_string(): string +func get_string() unsafe.Pointer { + return frankenphp.PHPString("a string", false) +} + +// export_php:function emit_event(array $userContext, array $event, string $from): int +func emit_event(userVal *C.zval, event *C.zval, fromStr *C.zend_string) int64 { + userArr := frankenphp.GoArray(unsafe.Pointer(userVal)) + user := helpers.GetUserContext(userArr) + if user.UserId == "" || len(user.Roles) == 0 { + helpers.ThrowPHPException("User context is missing userId or roles") + return 0 + } + + from := ids.ParseStateId(frankenphp.GoString(unsafe.Pointer(fromStr))) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + eventArr := frankenphp.GoArray(unsafe.Pointer(event)) + ev, err := helpers.ParseEvent(eventArr) + if err != nil { + helpers.ThrowPHPException(err.Error()) + return 0 + } + + operation := auth.Operation(ev.TargetOps) + preventCreation := true + switch operation { + case auth.Lock: + fallthrough + case auth.Call: + fallthrough + case auth.Signal: + fallthrough + case auth.Output: + preventCreation = false + } + + authd, err := Authorize(ctx, ev, from, preventCreation, operation) + if err != nil { + helpers.ThrowPHPException(err.Error()) + return 0 + } + if !authd { + helpers.ThrowPHPException("Resource not found") + } + replyTo := "" + if ev.ReplyTo != "" { + replyTo = ids.ParseStateId(ev.ReplyTo).ToSubject().String() + } + + splitType := strings.Split(ev.EventType, "\\") + eventType := splitType[len(splitType)-1] + + destinationId := ids.ParseStateId(ev.Destination) + + now, err := time.Now().MarshalText() + if err != nil { + helpers.ThrowPHPException(err.Error()) + return 0 + } + + userJson, err := json.Marshal(user) + if err != nil { + helpers.ThrowPHPException(err.Error()) + return 0 + } + + header := make(nats.Header) + header.Add(string(glue.HeaderStateId), destinationId.String()) + header.Add(string(glue.HeaderEventType), eventType) + header.Add(string(glue.HeaderTargetType), ev.TargetType) + header.Add(string(glue.HeaderEmittedAt), string(now)) + header.Add(string(glue.HeaderProvenance), string(userJson)) + header.Add(string(glue.HeaderTargetOps), ev.TargetOps) + header.Add(string(glue.HeaderSourceOps), ev.SourceOps) + header.Add(string(glue.HeaderMeta), ev.Meta) + header.Add(string(glue.HeaderEmittedBy), from.String()) + + msg := &nats.Msg{ + Subject: destinationId.ToSubject().String(), + Reply: replyTo, + Header: header, + Data: []byte(ev.Event), + } + + if ev.ScheduleAt.After(time.Now()) { + msg.Header.Add(string(glue.HeaderDelay), ev.ScheduleAt.Format(time.RFC3339)) + } + + ack, err := helpers.Js.PublishMsg(ctx, msg) + if err != nil { + helpers.ThrowPHPException(err.Error()) + return 0 + } + return int64(ack.Sequence) +} diff --git a/cli/ext/helpers/helpers.go b/cli/ext/helpers/helpers.go new file mode 100644 index 00000000..919a935d --- /dev/null +++ b/cli/ext/helpers/helpers.go @@ -0,0 +1,100 @@ +package helpers + +/* +#include +#include + +static inline void throw_exception(const char* msg) { + zend_throw_exception(zend_ce_exception, msg, 0); +} +*/ +import "C" +import ( + "github.com/bottledcode/durable-php/cli/auth" + "github.com/bottledcode/durable-php/cli/config" + "github.com/bottledcode/durable-php/cli/glue" + "github.com/dunglas/frankenphp" + "github.com/nats-io/nats.go/jetstream" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "os" + "time" + "unsafe" +) + +func ThrowPHPException(msg string) { + cstr := C.CString(msg) + defer C.free(unsafe.Pointer(cstr)) + C.throw_exception(cstr) +} + +func GetLogger(level zapcore.Level) *zap.Logger { + atom := zap.NewAtomicLevel() + atom.SetLevel(level) + + cfg := zap.NewDevelopmentEncoderConfig() + core := zapcore.NewCore(zapcore.NewConsoleEncoder(cfg), os.Stderr, atom) + return zap.New(core) +} + +var Logger *zap.Logger +var NatsState string +var Js jetstream.JetStream +var Config *config.Config + +func ParseEvent(arr *frankenphp.Array) (ev *glue.EventMessage, err error) { + ev = &glue.EventMessage{} + + for i := uint32(0); i < arr.Len(); i++ { + k, v := arr.At(i) + if k.Type == frankenphp.PHPIntKey { + ThrowPHPException("Event cannot contain integer keys") + } + switch k.Str { + case "eventId": + ev.EventId = v.(string) + case "event": + ev.Event = v.(string) + case "eventType": + ev.EventType = v.(string) + case "destination": + ev.Destination = v.(string) + case "meta": + ev.Meta = v.(string) + case "replyTo": + ev.ReplyTo = v.(string) + case "sourceOps": + ev.SourceOps = v.(string) + case "targetOps": + ev.TargetOps = v.(string) + case "targetType": + ev.TargetType = v.(string) + case "scheduleAt": + ev.ScheduleAt, err = time.Parse(time.RFC3339, v.(string)) + default: + ThrowPHPException("Unknown event key: " + k.Str) + } + } + return +} + +func GetUserContext(arr *frankenphp.Array) *auth.User { + user := &auth.User{} + + for i := uint32(0); i < arr.Len(); i++ { + k, v := arr.At(i) + if k.Type == frankenphp.PHPStringKey { + if k.Str == "userId" { + user.UserId = auth.UserId(v.(string)) + } + if k.Str == "roles" { + for j := uint32(0); j < v.(*frankenphp.Array).Len(); j++ { + _, n := v.(*frankenphp.Array).At(j) + user.Roles = append(user.Roles, auth.Role(n.(string))) + } + } + } + } + + return user +} diff --git a/cli/glue/glue.go b/cli/glue/glue.go index f3f8e317..96f05fef 100644 --- a/cli/glue/glue.go +++ b/cli/glue/glue.go @@ -3,10 +3,10 @@ package glue import ( "bytes" "context" - "durable_php/appcontext" - "durable_php/ids" "encoding/json" "fmt" + "github.com/bottledcode/durable-php/cli/appcontext" + "github.com/bottledcode/durable-php/cli/ids" "github.com/dunglas/frankenphp" "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" diff --git a/cli/glue/response_writer.go b/cli/glue/response_writer.go index 4d57346e..f3ef858c 100644 --- a/cli/glue/response_writer.go +++ b/cli/glue/response_writer.go @@ -4,9 +4,9 @@ import ( "bufio" "bytes" "context" - "durable_php/appcontext" - "durable_php/ids" "encoding/json" + "github.com/bottledcode/durable-php/cli/appcontext" + "github.com/bottledcode/durable-php/cli/ids" "github.com/nats-io/nats.go" "go.uber.org/zap" "net/http" diff --git a/cli/glue/state.go b/cli/glue/state.go index 50693ffc..fb98433a 100644 --- a/cli/glue/state.go +++ b/cli/glue/state.go @@ -2,7 +2,7 @@ package glue import ( "context" - "durable_php/ids" + "github.com/bottledcode/durable-php/cli/ids" "github.com/nats-io/nats.go/jetstream" ) diff --git a/cli/go.mod b/cli/go.mod index cb5a75a3..8f30a8c7 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -1,4 +1,4 @@ -module durable_php +module github.com/bottledcode/durable-php/cli go 1.24.5 @@ -6,7 +6,7 @@ require github.com/dunglas/frankenphp v1.9.0 require github.com/nats-io/nats.go v1.44.0 -require github.com/nats-io/nats-server/v2 v2.11.6 +require github.com/nats-io/nats-server/v2 v2.11.7 require github.com/teris-io/cli v1.0.1 @@ -39,9 +39,9 @@ require ( github.com/nats-io/jwt/v2 v2.7.4 // indirect github.com/nats-io/nkeys v0.4.11 // indirect github.com/nats-io/nuid v1.0.1 // indirect - github.com/oapi-codegen/runtime v1.1.1 // indirect + github.com/oapi-codegen/runtime v1.1.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_golang v1.22.0 // indirect + github.com/prometheus/client_golang v1.23.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.65.0 // indirect github.com/prometheus/procfs v0.17.0 // indirect diff --git a/cli/go.sum b/cli/go.sum index f1c57d20..df6b20f3 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -48,20 +48,20 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/nats-io/jwt/v2 v2.7.4 h1:jXFuDDxs/GQjGDZGhNgH4tXzSUK6WQi2rsj4xmsNOtI= github.com/nats-io/jwt/v2 v2.7.4/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA= -github.com/nats-io/nats-server/v2 v2.11.6 h1:4VXRjbTUFKEB+7UoaKL3F5Y83xC7MxPoIONOnGgpkHw= -github.com/nats-io/nats-server/v2 v2.11.6/go.mod h1:2xoztlcb4lDL5Blh1/BiukkKELXvKQ5Vy29FPVRBUYs= +github.com/nats-io/nats-server/v2 v2.11.7 h1:lINWQ/Hb3cnaoHmWTjj/7WppZnaSh9C/1cD//nHCbms= +github.com/nats-io/nats-server/v2 v2.11.7/go.mod h1:DchDPVzAsAPqhqm7VLedX0L7hjnV/SYtlmsl9F8U53s= github.com/nats-io/nats.go v1.44.0 h1:ECKVrDLdh/kDPV1g0gAQ+2+m2KprqZK5O/eJAyAnH2M= github.com/nats-io/nats.go v1.44.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0= github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= -github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= +github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI= +github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= -github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= +github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= diff --git a/cli/lib/api.go b/cli/lib/api.go index 90456c10..13bc247e 100644 --- a/cli/lib/api.go +++ b/cli/lib/api.go @@ -2,12 +2,12 @@ package lib import ( "context" - "durable_php/auth" - "durable_php/config" - "durable_php/glue" - "durable_php/ids" "encoding/json" "fmt" + "github.com/bottledcode/durable-php/cli/auth" + "github.com/bottledcode/durable-php/cli/config" + "github.com/bottledcode/durable-php/cli/glue" + "github.com/bottledcode/durable-php/cli/ids" "github.com/dunglas/frankenphp" "github.com/google/uuid" "github.com/gorilla/mux" diff --git a/cli/lib/billing.go b/cli/lib/billing.go index c19a3422..a1a23187 100644 --- a/cli/lib/billing.go +++ b/cli/lib/billing.go @@ -2,11 +2,11 @@ package lib import ( "context" - "durable_php/config" - "durable_php/glue" - "durable_php/ids" "encoding/json" "fmt" + "github.com/bottledcode/durable-php/cli/config" + "github.com/bottledcode/durable-php/cli/glue" + "github.com/bottledcode/durable-php/cli/ids" "github.com/nats-io/nats.go/jetstream" "go.uber.org/zap" "time" diff --git a/cli/lib/consumer.go b/cli/lib/consumer.go index c68dfb81..27ff8997 100644 --- a/cli/lib/consumer.go +++ b/cli/lib/consumer.go @@ -2,13 +2,13 @@ package lib import ( "context" - "durable_php/appcontext" - "durable_php/auth" - "durable_php/config" - "durable_php/glue" - "durable_php/ids" "encoding/json" "fmt" + "github.com/bottledcode/durable-php/cli/appcontext" + "github.com/bottledcode/durable-php/cli/auth" + "github.com/bottledcode/durable-php/cli/config" + "github.com/bottledcode/durable-php/cli/glue" + "github.com/bottledcode/durable-php/cli/ids" "github.com/nats-io/nats.go/jetstream" "go.uber.org/zap" "net/http" diff --git a/cli/lib/index.go b/cli/lib/index.go index 0f5e11ad..83755fd5 100644 --- a/cli/lib/index.go +++ b/cli/lib/index.go @@ -2,7 +2,7 @@ package lib import ( "context" - "durable_php/config" + "github.com/bottledcode/durable-php/cli/config" "github.com/typesense/typesense-go/typesense" "github.com/typesense/typesense-go/typesense/api" "github.com/typesense/typesense-go/typesense/api/pointer" diff --git a/cli/lib/indexer.go b/cli/lib/indexer.go index 7cfe0b3f..2027178d 100644 --- a/cli/lib/indexer.go +++ b/cli/lib/indexer.go @@ -2,10 +2,10 @@ package lib import ( "context" - "durable_php/config" - "durable_php/glue" - "durable_php/ids" "encoding/json" + "github.com/bottledcode/durable-php/cli/config" + "github.com/bottledcode/durable-php/cli/glue" + "github.com/bottledcode/durable-php/cli/ids" "github.com/nats-io/nats.go/jetstream" "github.com/typesense/typesense-go/typesense" "github.com/typesense/typesense-go/typesense/api" diff --git a/cli/lib/locks.go b/cli/lib/locks.go index 2b72b41f..225ed98e 100644 --- a/cli/lib/locks.go +++ b/cli/lib/locks.go @@ -2,8 +2,8 @@ package lib import ( "context" - "durable_php/ids" "errors" + "github.com/bottledcode/durable-php/cli/ids" "github.com/nats-io/nats.go/jetstream" "go.uber.org/zap" "time" diff --git a/cli/test.http b/cli/test.http new file mode 100644 index 00000000..bd6e575c --- /dev/null +++ b/cli/test.http @@ -0,0 +1 @@ +GET localhost:8080/test.php \ No newline at end of file diff --git a/cli/test.php b/cli/test.php new file mode 100644 index 00000000..0bcd5e8b --- /dev/null +++ b/cli/test.php @@ -0,0 +1,10 @@ + 'bob', 'roles' => ['admin']], [], 'activity:bullshit'); +try { + echo emit_event([]); +} catch (Throwable $e) { + echo "\nFailed: $e"; +} diff --git a/frankenphp-ext/ext/client.go b/frankenphp-ext/ext/client.go new file mode 100644 index 00000000..32e86273 --- /dev/null +++ b/frankenphp-ext/ext/client.go @@ -0,0 +1,34 @@ +package ext + +import ( + "C" + "github.com/dunglas/frankenphp" + "strings" + "unsafe" +) + +func init() { + frankenphp.RegisterExtension(unsafe.Pointer(&C.ext_module_entry)) +} + +// export_php:module init +func moduleInit() { + // this is during init +} + +// export_php:function repeat_this(string $id, string $str, int $count, bool $reverse): string +func repeat_this(idStr *C.zend_string, s *C.zend_string, count int, reverse bool) unsafe.Pointer { + str := frankenphp.GoString(unsafe.Pointer(s)) + + result := strings.Repeat(str, count) + + if reverse { + runes := []rune(result) + for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { + runes[i], runes[j] = runes[j], runes[i] + } + result = string(runes) + } + + return frankenphp.PHPString(result, false) +} From bd782cceeb51949ad91713933e31efaec2b7361b Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Tue, 5 Aug 2025 18:55:58 +0200 Subject: [PATCH 02/19] remove defers Signed-off-by: Robert Landers --- cli/ext/ext.go | 13 ++++++++----- cli/ext/helpers/helpers.go | 2 ++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/cli/ext/ext.go b/cli/ext/ext.go index 32ebe3b0..cb8a2ae5 100644 --- a/cli/ext/ext.go +++ b/cli/ext/ext.go @@ -31,7 +31,11 @@ This is a php extension that operates as a client for durable php // export_php:namespace Bottledcode\DurablePhp\Ext +// export_php:module shutdown func go_shutdown_module() { + if helpers.NatServer != nil { + helpers.NatServer.Shutdown() + } os.RemoveAll(helpers.NatsState) } @@ -56,7 +60,7 @@ func go_init_module() { panic(err) } - s := test.RunServer(&server.Options{ + helpers.NatServer = test.RunServer(&server.Options{ Host: "localhost", Port: 4222, NoLog: true, @@ -66,7 +70,6 @@ func go_init_module() { StoreDir: helpers.NatsState, HTTPPort: 8222, }) - defer s.Shutdown() boostrapNats = true } @@ -259,7 +262,7 @@ func go_init_module() { if err != nil { panic(err) } - defer consume.Drain() + //defer consume.Drain() orchestrationConsumer, err := billingStream.CreateOrUpdateConsumer(ctx, jetstream.ConsumerConfig{ Durable: "orchestrationAggregator", @@ -276,7 +279,7 @@ func go_init_module() { if err != nil { panic(err) } - defer consume.Drain() + //defer consume.Drain() activityConsumer, err := billingStream.CreateOrUpdateConsumer(ctx, jetstream.ConsumerConfig{ Durable: "activityAggregator", @@ -299,7 +302,7 @@ func go_init_module() { if err != nil { panic(err) } - defer consume.Drain() + //defer consume.Drain() } err := lib.StartBillingProcessor(ctx, cfg, helpers.Js, logger) diff --git a/cli/ext/helpers/helpers.go b/cli/ext/helpers/helpers.go index 919a935d..bd9c5c6c 100644 --- a/cli/ext/helpers/helpers.go +++ b/cli/ext/helpers/helpers.go @@ -14,6 +14,7 @@ import ( "github.com/bottledcode/durable-php/cli/config" "github.com/bottledcode/durable-php/cli/glue" "github.com/dunglas/frankenphp" + "github.com/nats-io/nats-server/v2/server" "github.com/nats-io/nats.go/jetstream" "go.uber.org/zap" "go.uber.org/zap/zapcore" @@ -41,6 +42,7 @@ var Logger *zap.Logger var NatsState string var Js jetstream.JetStream var Config *config.Config +var NatServer *server.Server func ParseEvent(arr *frankenphp.Array) (ev *glue.EventMessage, err error) { ev = &glue.EventMessage{} From 091c9653acd8d4a8d788d0af644295fd9b0f21b5 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Tue, 5 Aug 2025 18:57:14 +0200 Subject: [PATCH 03/19] return after throw Signed-off-by: Robert Landers --- cli/ext/ext.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/ext/ext.go b/cli/ext/ext.go index cb8a2ae5..50f97996 100644 --- a/cli/ext/ext.go +++ b/cli/ext/ext.go @@ -386,6 +386,7 @@ func emit_event(userVal *C.zval, event *C.zval, fromStr *C.zend_string) int64 { } if !authd { helpers.ThrowPHPException("Resource not found") + return 0 } replyTo := "" if ev.ReplyTo != "" { From 7e3f8f986691b57e5050a7ae5523d56003418924 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Wed, 6 Aug 2025 08:41:21 +0200 Subject: [PATCH 04/19] continue with extension Signed-off-by: Robert Landers --- cli/Makefile | 9 +- cli/ext/ext.go | 247 ++++++++++++++++++++++++--- cli/ext/helpers/helpers.go | 32 +++- cli/glue/glue.go | 151 ++++++++++++++++- cli/go.mod | 14 ++ cli/go.sum | 45 +++++ cli/lib/api.go | 34 ++-- cli/lib/consumer.go | 290 ++++++++++++++++++++++++++++++-- cli/test.php | 28 ++- src/Events/EventDescription.php | 14 +- src/State/Ids/StateId.php | 4 +- 11 files changed, 797 insertions(+), 71 deletions(-) diff --git a/cli/Makefile b/cli/Makefile index 40cb3f3a..ef9e78a5 100644 --- a/cli/Makefile +++ b/cli/Makefile @@ -1,9 +1,14 @@ -# Variables +DEBUG ?= 0 + PHP_INCLUDES := $(shell php-config --includes) PHP_LDFLAGS := $(shell php-config --ldflags) PHP_LIBS := $(shell php-config --libs) -XCADDY_FLAGS := -ldflags='-w -s' -tags=nobadger,nomysql,nopgx,nodphp,nobrotli +ifeq ($(DEBUG),1) + XCADDY_FLAGS := -gcflags='all=-N -l' -tags=nobadger,nomysql,nopgx,nodphp,nobrotli +else + XCADDY_FLAGS := -ldflags='-w -s' -tags=nobadger,nomysql,nopgx,nodphp,nobrotli +endif LOCAL_MODULE := /home/withinboredom/code/durable-php/cli diff --git a/cli/ext/ext.go b/cli/ext/ext.go index 50f97996..c2415877 100644 --- a/cli/ext/ext.go +++ b/cli/ext/ext.go @@ -6,6 +6,13 @@ import ( "context" "encoding/json" "errors" + "os" + "strings" + "sync" + "time" + "unsafe" + + "github.com/bottledcode/durable-php/cli/appcontext" "github.com/bottledcode/durable-php/cli/auth" "github.com/bottledcode/durable-php/cli/config" "github.com/bottledcode/durable-php/cli/ext/helpers" @@ -18,11 +25,6 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" "go.uber.org/zap" - "os" - "strings" - "sync" - "time" - "unsafe" ) /** @@ -36,6 +38,7 @@ func go_shutdown_module() { if helpers.NatServer != nil { helpers.NatServer.Shutdown() } + // remove nats state directory os.RemoveAll(helpers.NatsState) } @@ -52,6 +55,8 @@ func go_init_module() { logger.Info("Starting Durable PHP") + helpers.Ctx = context.WithValue(context.Background(), "bootstrap", cfg.Bootstrap) + boostrapNats := cfg.Nat.Bootstrap if cfg.Nat.Internal { logger.Warn("Running in dev mode, all data will be deleted at the end of this") @@ -255,7 +260,7 @@ func go_init_module() { panic(err) } - consume, err := entityConsumer.Consume(func(msg jetstream.Msg) { + _, err = entityConsumer.Consume(func(msg jetstream.Msg) { incrementInt("e", 1) msg.Ack() }) @@ -272,7 +277,7 @@ func go_init_module() { panic(err) } - consume, err = orchestrationConsumer.Consume(func(msg jetstream.Msg) { + _, err = orchestrationConsumer.Consume(func(msg jetstream.Msg) { incrementInt("o", 1) msg.Ack() }) @@ -289,7 +294,7 @@ func go_init_module() { panic(err) } - consume, err = activityConsumer.Consume(func(msg jetstream.Msg) { + _, err = activityConsumer.Consume(func(msg jetstream.Msg) { incrementInt("ac", 1) var ev lib.BillingEvent err := json.Unmarshal(msg.Data(), &ev) @@ -340,25 +345,227 @@ func Authorize(ctx context.Context, ev *glue.EventMessage, from *ids.StateId, pr return true, nil } -// export_php:function get_string(): string -func get_string() unsafe.Pointer { - return frankenphp.PHPString("a string", false) +// export_php:method Worker::startEventLoop(): void +func (w *Worker) startEventLoop(kindStr *C.zend_string) { + if w.started { + helpers.ThrowPHPException("Event loop already running") + return + } + + ctx, done := context.WithCancel(helpers.Ctx) + + c := &helpers.Consumer{ + Context: ctx, + Done: done, + } + + stream, err := helpers.Js.Stream(ctx, helpers.Config.Stream) + if err != nil { + helpers.ThrowPHPException(err.Error()) + return + } + + c.Msg = lib.StartConsumer(ctx, helpers.Config, stream, helpers.Logger, w.kind) + w.consumer = c } -// export_php:function emit_event(array $userContext, array $event, string $from): int -func emit_event(userVal *C.zval, event *C.zval, fromStr *C.zend_string) int64 { - userArr := frankenphp.GoArray(unsafe.Pointer(userVal)) - user := helpers.GetUserContext(userArr) - if user.UserId == "" || len(user.Roles) == 0 { - helpers.ThrowPHPException("User context is missing userId or roles") - return 0 +// export_php:method Worker::drainEventLoop(): void +func (w *Worker) drainEventLoop() { + if !w.started { + return } + w.consumer.Msg.Drain() + w.started = false +} - from := ids.ParseStateId(frankenphp.GoString(unsafe.Pointer(fromStr))) +// export_php:method Worker::__destruct(): void +func (w *Worker) __destruct() { + w.consumer.Msg.Stop() + w.consumer.Done() +} + +// export_php:class Worker +type Worker struct { + kind ids.IdKind + started bool + consumer *helpers.Consumer + activeId *ids.StateId + state *glue.StateArray + pendingEvents []*frankenphp.Array + authContext []byte + currentCtx context.Context + currentMsg jetstream.Msg +} + +// export_php:method Worker::__construct(string $kind): void +func (w *Worker) __construct(kindStr *C.zend_string) { + kind := ids.IdKind(frankenphp.GoString(unsafe.Pointer(kindStr))) + + switch kind { + case ids.Activity: + case ids.Entity: + case ids.Orchestration: + default: + helpers.ThrowPHPException("Invalid event kind") + return + } + w.kind = kind +} + +// export_php:method Worker::getNextEvent(): ?string +func (w *Worker) getNextEvent() unsafe.Pointer { + c := w.consumer + + ctx := c.Context + logger := helpers.Logger + js := helpers.Js + + msg, err := c.Msg.Next() + if err != nil { + helpers.ThrowPHPException(err.Error()) + return frankenphp.PHPString("", false) + } + + meta, _ := msg.Metadata() + headers := msg.Headers() + + currentUser := &auth.User{} + b := msg.Headers().Get(string(glue.HeaderProvenance)) + err = json.Unmarshal([]byte(b), currentUser) + if err != nil { + logger.Warn("Failed to unmarshal event provenance", + zap.Any("Provenance", msg.Headers().Get(string(glue.HeaderProvenance))), + zap.Error(err), + ) + currentUser = nil + } else { + ctx = auth.DecorateContextWithUser(ctx, currentUser) + } + + if headers.Get(string(glue.HeaderDelay)) != "" && meta.NumDelivered == 1 { + logger.Debug("Delaying message", zap.String("delay", msg.Headers().Get("Delay")), zap.Any("Headers", meta)) + schedule, err := time.Parse(time.RFC3339, msg.Headers().Get("Delay")) + if err != nil { + helpers.ThrowPHPException(err.Error()) + return frankenphp.PHPString("", false) + } + + delay := time.Until(schedule) + if err := msg.NakWithDelay(delay); err != nil { + helpers.ThrowPHPException(err.Error()) + return frankenphp.PHPString("", false) + } + + return w.getNextEvent() + } + + if strings.HasSuffix(msg.Subject(), ".delete") { + id := ids.ParseStateId(msg.Headers().Get(string(glue.HeaderStateId))) + // todo: remove glue! + err := glue.DeleteState(ctx, js, logger, id) + if err != nil { + helpers.ThrowPHPException(err.Error()) + return frankenphp.PHPString("", false) + } + return w.getNextEvent() + } + + w.currentCtx = lib.GetCorrelationId(ctx, nil, &headers) + + rm := auth.GetResourceManager(ctx, js) + + w.authContext, w.activeId, w.state, err = lib.ProcessMessage(ctx, logger, msg, rm, helpers.Config, js) + if err != nil { + helpers.ThrowPHPException(err.Error()) + return frankenphp.PHPString("", false) + } + + w.currentMsg = msg + + return frankenphp.PHPString(string(msg.Data()), false) +} + +// export_php:method Worker::queryState(string $stateId): array +func (w *Worker) queryState(idStr unsafe.Pointer) unsafe.Pointer { + id := ids.ParseStateId(frankenphp.GoString(idStr)) + state, err := glue.GetStateArray(id, helpers.Js, w.currentCtx, helpers.Logger) + if err != nil { + helpers.ThrowPHPException(err.Error()) + return nil + } + return frankenphp.PHPArray(state.Data.Array) +} + +// export_php:method Worker::getUser(): ?array +func (w *Worker) getUser() unsafe.Pointer { + if provenance, ok := w.currentCtx.Value(appcontext.CurrentUserKey).(*auth.User); ok { + ret := &glue.Array{} + ret.SetString("user", string(provenance.UserId)) + roles := &frankenphp.Array{} + for _, r := range provenance.Roles { + roles.Append(string(r)) + } + ret.SetString("roles", roles) - ctx, cancel := context.WithCancel(context.Background()) + return frankenphp.PHPArray(ret.Array) + } + + return nil +} + +// export_php:method Worker::getSource(): string +func (w *Worker) getSource() unsafe.Pointer { + sourceId := ids.ParseStateId(w.currentMsg.Headers().Get(string(glue.HeaderEmittedBy))) + return frankenphp.PHPString(sourceId.String(), false) +} + +// export_php:method Worker::getCurrentId(): string +func (w *Worker) getCurrentId() unsafe.Pointer { + return frankenphp.PHPString(w.activeId.String(), false) +} + +// export_php:method Worker::getCorrelationId(): string +func (w *Worker) getCorrelationId() unsafe.Pointer { + return frankenphp.PHPString(w.currentCtx.Value("cid").(string), false) +} + +// export_php:method Worker::getState(): ?array +func (w *Worker) getState() unsafe.Pointer { + return frankenphp.PHPArray(w.state.Data.Array) +} + +// export_php:method Worker::updateState(array $state): void +func (w *Worker) updateState(state unsafe.Pointer) { + arr := frankenphp.GoArray(state) + w.state.Data.Array = arr +} + +// export_php:method Worker::emitEvent(array $eventDescription): void +func (w *Worker) emitEvent(event unsafe.Pointer) { + arr := frankenphp.GoArray(event) + w.pendingEvents = append(w.pendingEvents, arr) +} + +func (w *Worker) deleteState() {} + +// export_php:function emit_event(?array $userContext, array $event, string $from): int +func emit_event(userVal *C.zval, event *C.zval, fromStr *C.zend_string) int64 { + var user *auth.User + ctx, cancel := context.WithCancel(helpers.Ctx) defer cancel() + if userVal != nil { + userArr := frankenphp.GoArray(unsafe.Pointer(userVal)) + user = helpers.GetUserContext(userArr) + if user.UserId == "" || len(user.Roles) == 0 { + helpers.ThrowPHPException("User context is missing userId or roles") + return 0 + } + ctx = auth.DecorateContextWithUser(ctx, user) + } + + from := ids.ParseStateId(frankenphp.GoString(unsafe.Pointer(fromStr))) + eventArr := frankenphp.GoArray(unsafe.Pointer(event)) ev, err := helpers.ParseEvent(eventArr) if err != nil { diff --git a/cli/ext/helpers/helpers.go b/cli/ext/helpers/helpers.go index bd9c5c6c..e0e3ca69 100644 --- a/cli/ext/helpers/helpers.go +++ b/cli/ext/helpers/helpers.go @@ -10,6 +10,12 @@ static inline void throw_exception(const char* msg) { */ import "C" import ( + "context" + "errors" + "os" + "time" + "unsafe" + "github.com/bottledcode/durable-php/cli/auth" "github.com/bottledcode/durable-php/cli/config" "github.com/bottledcode/durable-php/cli/glue" @@ -18,9 +24,6 @@ import ( "github.com/nats-io/nats.go/jetstream" "go.uber.org/zap" "go.uber.org/zap/zapcore" - "os" - "time" - "unsafe" ) func ThrowPHPException(msg string) { @@ -38,11 +41,29 @@ func GetLogger(level zapcore.Level) *zap.Logger { return zap.New(core) } +type Consumer struct { + Context context.Context + Done context.CancelFunc + Msg jetstream.MessagesContext +} + var Logger *zap.Logger var NatsState string var Js jetstream.JetStream var Config *config.Config var NatServer *server.Server +var Ctx context.Context + +func ParseStateId(arr *frankenphp.Array) (id string, err error) { + _, idx := arr.At(0) + ok := false + if id, ok = idx.(string); ok { + return + } + Logger.Warn("Failed to parse state id", zap.Any("id", idx)) + ThrowPHPException("Failed to parse state id") + return "", errors.New("Failed to parse state id") +} func ParseEvent(arr *frankenphp.Array) (ev *glue.EventMessage, err error) { ev = &glue.EventMessage{} @@ -72,9 +93,12 @@ func ParseEvent(arr *frankenphp.Array) (ev *glue.EventMessage, err error) { case "targetType": ev.TargetType = v.(string) case "scheduleAt": - ev.ScheduleAt, err = time.Parse(time.RFC3339, v.(string)) + if str, ok := v.(string); ok { + ev.ScheduleAt, err = time.Parse(time.RFC3339, str) + } default: ThrowPHPException("Unknown event key: " + k.Str) + return nil, errors.New("Unknown event key: " + k.Str) } } return diff --git a/cli/glue/glue.go b/cli/glue/glue.go index 96f05fef..9919ecd4 100644 --- a/cli/glue/glue.go +++ b/cli/glue/glue.go @@ -5,12 +5,6 @@ import ( "context" "encoding/json" "fmt" - "github.com/bottledcode/durable-php/cli/appcontext" - "github.com/bottledcode/durable-php/cli/ids" - "github.com/dunglas/frankenphp" - "github.com/nats-io/nats.go" - "github.com/nats-io/nats.go/jetstream" - "go.uber.org/zap" "io" "net/http" "net/url" @@ -18,6 +12,13 @@ import ( "path/filepath" "strings" "sync" + + "github.com/bottledcode/durable-php/cli/appcontext" + "github.com/bottledcode/durable-php/cli/ids" + "github.com/dunglas/frankenphp" + "github.com/nats-io/nats.go" + "github.com/nats-io/nats.go/jetstream" + "go.uber.org/zap" ) type Method string @@ -253,6 +254,144 @@ func DeleteState(ctx context.Context, stream jetstream.JetStream, logger *zap.Lo return nil } +type Array struct { + *frankenphp.Array +} + +func (a *Array) GetStringKey(key string) interface{} { + for i := uint32(0); i < a.Len(); i++ { + k, v := a.At(i) + if k.Type == frankenphp.PHPStringKey && k.Str == key { + return v + } + } + + return nil +} + +func (a *Array) Unmarshall(j []byte) error { + result := Array{} + err := json.Unmarshal(j, &result.Array) + if err != nil { + return err + } + a.Array = result.Array + return nil +} + +func (a *Array) Marshal() ([]byte, error) { + data, err := json.Marshal(a.Array) + return data, err +} + +type StateArray struct { + Creating bool + Data *Array + Revision uint64 +} + +func GetStateArray(id *ids.StateId, stream jetstream.JetStream, ctx context.Context, logger *zap.Logger) (*StateArray, error) { + if id.Kind == ids.Orchestration { + bucket, err := stream.CreateOrUpdateKeyValue(ctx, jetstream.KeyValueConfig{ + Bucket: string(ids.Orchestration), + Description: "Holds orchestration state and history", + Compression: true, + }) + if err != nil { + return nil, err + } + arr := &StateArray{ + Data: &Array{}, + } + + get, err := bucket.Get(ctx, id.ToSubject().String()) + if err == nil { + result := &Array{} + err = result.Unmarshall(get.Value()) + if err != nil { + return nil, err + } + arr.Data = result + arr.Revision = get.Revision() + return arr, nil + } + arr.Creating = true + return arr, nil + } + + obj, err := GetObjectStore(id.Kind, stream, ctx) + if err != nil { + return nil, err + } + result := &StateArray{ + Data: &Array{}, + } + + res, err := obj.GetBytes(ctx, id.ToSubject().String()) + if err != nil { + result.Creating = true + return result, nil + } + + err = result.Data.Unmarshall(res) + if err != nil { + return nil, err + } + + return result, nil +} + +func (arr *StateArray) Update(id *ids.StateId, stream jetstream.JetStream, ctx context.Context, logger *zap.Logger) error { + if id.Kind == ids.Orchestration { + bucket, err := stream.CreateOrUpdateKeyValue(ctx, jetstream.KeyValueConfig{ + Bucket: string(ids.Orchestration), + Description: "Holds orchestration state and history", + Compression: true, + }) + if err != nil { + return err + } + dataBytes, err := arr.Data.Marshal() + if err != nil { + return err + } + + if arr.Creating { + _, err = bucket.Create(ctx, id.ToSubject().String(), dataBytes) + if err != nil { + return err + } + } else { + _, err = bucket.Update(ctx, id.ToSubject().String(), dataBytes, arr.Revision) + if err != nil { + return err + } + } + + return nil + } + + obj, err := GetObjectStore(id.Kind, stream, ctx) + if err != nil { + return err + } + dataBytes, err := arr.Data.Marshal() + if err != nil { + return err + } + info, err := obj.PutBytes(ctx, id.ToSubject().String(), dataBytes) + if err != nil { + return err + } + + _, err = obj.AddLink(ctx, id.ToSubject().String(), info) + if err != nil { + return err + } + + return nil +} + func GetStateFile(id *ids.StateId, stream jetstream.JetStream, ctx context.Context, logger *zap.Logger) (*os.File, func() error) { if id.Kind == ids.Orchestration { // orchestrations use optimistic concurrency and the kv store for state diff --git a/cli/go.mod b/cli/go.mod index 8f30a8c7..4970ddcd 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -31,8 +31,12 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/dolthub/maphash v0.1.0 // indirect github.com/gammazero/deque v1.1.0 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/google/go-tpm v0.9.5 // indirect + github.com/google/gops v0.3.28 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/compress v1.18.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/maypok86/otter v1.2.4 // indirect github.com/minio/highwayhash v1.0.3 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect @@ -41,15 +45,25 @@ require ( github.com/nats-io/nuid v1.0.1 // indirect github.com/oapi-codegen/runtime v1.1.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/prometheus/client_golang v1.23.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.65.0 // indirect github.com/prometheus/procfs v0.17.0 // indirect + github.com/shirou/gopsutil/v3 v3.23.7 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/sony/gobreaker v1.0.0 // indirect + github.com/spf13/cobra v1.7.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/tklauser/go-sysconf v0.3.11 // indirect + github.com/tklauser/numcpus v0.6.0 // indirect + github.com/xlab/treeprint v1.2.0 // indirect + github.com/yusufpapurcu/wmi v1.2.3 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.40.0 // indirect golang.org/x/sys v0.34.0 // indirect golang.org/x/time v0.12.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + rsc.io/goversion v1.2.0 // indirect ) diff --git a/cli/go.sum b/cli/go.sum index df6b20f3..61bb154c 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -8,6 +8,7 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -17,16 +18,24 @@ github.com/dunglas/frankenphp v1.9.0 h1:tucI7uSZEmwGRGg7JxAf3wTwLrYs319mSc6fATG9 github.com/dunglas/frankenphp v1.9.0/go.mod h1:jpmWK5Nmi2LkpgL+Td0+LQWRcQ5jVOYsuT9f+L7ohDs= github.com/gammazero/deque v1.1.0 h1:OyiyReBbnEG2PP0Bnv1AASLIYvyKqIFN5xfl1t8oGLo= github.com/gammazero/deque v1.1.0/go.mod h1:JVrR+Bj1NMQbPnYclvDlvSX0nVGReLrQZ0aUMuWLctg= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU= github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/gops v0.3.28 h1:2Xr57tqKAmQYRAfG12E+yLcoa2Y42UJo2lOrUFL9ark= +github.com/google/gops v0.3.28/go.mod h1:6f6+Nl8LcHrzJwi8+p0ii+vmBFSlB4f8cOOkTJ7sk4c= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jinzhu/copier v0.3.4 h1:mfU6jI9PtCeUjkjQ322dlff9ELjGDu975C2p/nrubVI= github.com/jinzhu/copier v0.3.4/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= @@ -38,6 +47,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/maypok86/otter v1.2.4 h1:HhW1Pq6VdJkmWwcZZq19BlEQkHtI8xgsQzBVXJU0nfc= github.com/maypok86/otter v1.2.4/go.mod h1:mKLfoI7v1HOmQMwFgX4QkRk23mX6ge3RDvjdHOWG4R4= github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q= @@ -60,6 +71,8 @@ github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQ github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= @@ -70,17 +83,41 @@ github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7D github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shirou/gopsutil/v3 v3.23.7 h1:C+fHO8hfIppoJ1WdsVm1RoI0RwXoNdfTK7yWXV0wVj4= +github.com/shirou/gopsutil/v3 v3.23.7/go.mod h1:c4gnmoRC0hQuaLqvxnx1//VXQ0Ms/X9UnJF8pddY5z4= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ= github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/teris-io/cli v1.0.1 h1:J6jnVHC552uqx7zT+Ux0++tIvLmJQULqxVhCid2u/Gk= github.com/teris-io/cli v1.0.1/go.mod h1:V9nVD5aZ873RU/tQXLSXO8FieVPQhQvuNohsdsKXsGw= +github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= +github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= +github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms= +github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= github.com/typesense/typesense-go v1.1.0 h1:QocehDarVXRArMIosPIdawiVFZZbnRkPJxwnAGOFkzw= github.com/typesense/typesense-go v1.1.0/go.mod h1:KcPODU7ltrcUFC/gygMTkAAfZ9M8/q6ayrdl1MnE1kI= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= @@ -95,6 +132,10 @@ golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= @@ -102,10 +143,14 @@ golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/goversion v1.2.0 h1:SPn+NLTiAG7w30IRK/DKp1BjvpWabYgxlLp/+kx5J8w= +rsc.io/goversion v1.2.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo= diff --git a/cli/lib/api.go b/cli/lib/api.go index 13bc247e..2a6ce2c1 100644 --- a/cli/lib/api.go +++ b/cli/lib/api.go @@ -36,7 +36,7 @@ func generateCorrelationId() string { return string(bytes) } -func getCorrelationId(ctx context.Context, hHeaders *http.Header, nHeaders *nats.Header) context.Context { +func GetCorrelationId(ctx context.Context, hHeaders *http.Header, nHeaders *nats.Header) context.Context { if ctx.Value("cid") != nil { return ctx } @@ -97,7 +97,7 @@ func Startup(ctx context.Context, js jetstream.JetStream, logger *zap.Logger, po } request.Header.Add("DPHP_BOOTSTRAP", config.Bootstrap) - ctx := getCorrelationId(ctx, &request.Header, nil) + ctx := GetCorrelationId(ctx, &request.Header, nil) logRequest(logger, request, ctx) request, err := frankenphp.NewRequestWithContext(request, frankenphp.WithRequestEnv(map[string]string{ @@ -134,7 +134,7 @@ func Startup(ctx context.Context, js jetstream.JetStream, logger *zap.Logger, po return } - ctx := getCorrelationId(ctx, &request.Header, nil) + ctx := GetCorrelationId(ctx, &request.Header, nil) logRequest(logger, request, ctx) store, err := glue.GetObjectStore("activities", js, context.Background()) @@ -161,7 +161,7 @@ func Startup(ctx context.Context, js jetstream.JetStream, logger *zap.Logger, po return } - ctx := getCorrelationId(ctx, &request.Header, nil) + ctx := GetCorrelationId(ctx, &request.Header, nil) logRequest(logger, request, ctx) vars := mux.Vars(request) @@ -198,7 +198,7 @@ func Startup(ctx context.Context, js jetstream.JetStream, logger *zap.Logger, po http.Error(writer, "Page should be integer", http.StatusBadRequest) } - ctx := getCorrelationId(ctx, &request.Header, nil) + ctx := GetCorrelationId(ctx, &request.Header, nil) logRequest(logger, request, ctx) if len(config.Extensions.Search.Collections) == 0 { @@ -338,7 +338,7 @@ func Startup(ctx context.Context, js jetstream.JetStream, logger *zap.Logger, po return } - ctx := getCorrelationId(ctx, &request.Header, nil) + ctx := GetCorrelationId(ctx, &request.Header, nil) logRequest(logger, request, ctx) vars := mux.Vars(request) @@ -392,7 +392,7 @@ func Startup(ctx context.Context, js jetstream.JetStream, logger *zap.Logger, po return } - ctx := getCorrelationId(ctx, &request.Header, nil) + ctx := GetCorrelationId(ctx, &request.Header, nil) logRequest(logger, request, ctx) vars := mux.Vars(request) @@ -470,7 +470,7 @@ func Startup(ctx context.Context, js jetstream.JetStream, logger *zap.Logger, po return } - ctx := getCorrelationId(ctx, &request.Header, nil) + ctx := GetCorrelationId(ctx, &request.Header, nil) logRequest(logger, request, ctx) vars := mux.Vars(request) @@ -531,7 +531,7 @@ func Startup(ctx context.Context, js jetstream.JetStream, logger *zap.Logger, po Id: strings.TrimSpace(vars["id"]), } - ctx := getCorrelationId(ctx, &request.Header, nil) + ctx := GetCorrelationId(ctx, &request.Header, nil) logRequest(logger, request, ctx) if request.Method == "GET" { @@ -611,7 +611,7 @@ func Startup(ctx context.Context, js jetstream.JetStream, logger *zap.Logger, po return } - ctx := getCorrelationId(ctx, &request.Header, nil) + ctx := GetCorrelationId(ctx, &request.Header, nil) logRequest(logger, request, ctx) http.Error(writer, "Method Not Allowed", http.StatusMethodNotAllowed) @@ -630,7 +630,7 @@ func Startup(ctx context.Context, js jetstream.JetStream, logger *zap.Logger, po return } - ctx := getCorrelationId(ctx, &request.Header, nil) + ctx := GetCorrelationId(ctx, &request.Header, nil) logRequest(logger, request, ctx) vars := mux.Vars(request) @@ -666,7 +666,7 @@ func Startup(ctx context.Context, js jetstream.JetStream, logger *zap.Logger, po return } - ctx := getCorrelationId(ctx, &request.Header, nil) + ctx := GetCorrelationId(ctx, &request.Header, nil) logRequest(logger, request, ctx) vars := mux.Vars(request) @@ -720,7 +720,7 @@ func Startup(ctx context.Context, js jetstream.JetStream, logger *zap.Logger, po return } - ctx := getCorrelationId(ctx, &request.Header, nil) + ctx := GetCorrelationId(ctx, &request.Header, nil) logRequest(logger, request, ctx) vars := mux.Vars(request) @@ -798,7 +798,7 @@ func Startup(ctx context.Context, js jetstream.JetStream, logger *zap.Logger, po return } - ctx := getCorrelationId(ctx, &request.Header, nil) + ctx := GetCorrelationId(ctx, &request.Header, nil) logRequest(logger, request, ctx) vars := mux.Vars(request) @@ -854,7 +854,7 @@ func Startup(ctx context.Context, js jetstream.JetStream, logger *zap.Logger, po vars := mux.Vars(request) - ctx := getCorrelationId(ctx, &request.Header, nil) + ctx := GetCorrelationId(ctx, &request.Header, nil) logRequest(logger, request, ctx) id := &ids.OrchestrationId{ @@ -984,7 +984,7 @@ func Startup(ctx context.Context, js jetstream.JetStream, logger *zap.Logger, po return } - ctx := getCorrelationId(ctx, &request.Header, nil) + ctx := GetCorrelationId(ctx, &request.Header, nil) logRequest(logger, request, ctx) vars := mux.Vars(request) @@ -1007,7 +1007,7 @@ func Startup(ctx context.Context, js jetstream.JetStream, logger *zap.Logger, po r.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) { logger.Warn("Unknown endpoint") - ctx := getCorrelationId(ctx, &request.Header, nil) + ctx := GetCorrelationId(ctx, &request.Header, nil) logRequest(logger, request, ctx) }) diff --git a/cli/lib/consumer.go b/cli/lib/consumer.go index 27ff8997..46e18988 100644 --- a/cli/lib/consumer.go +++ b/cli/lib/consumer.go @@ -4,6 +4,11 @@ import ( "context" "encoding/json" "fmt" + "net/http" + "runtime" + "strings" + "time" + "github.com/bottledcode/durable-php/cli/appcontext" "github.com/bottledcode/durable-php/cli/auth" "github.com/bottledcode/durable-php/cli/config" @@ -11,12 +16,24 @@ import ( "github.com/bottledcode/durable-php/cli/ids" "github.com/nats-io/nats.go/jetstream" "go.uber.org/zap" - "net/http" - "runtime" - "strings" - "time" ) +func StartConsumer(ctx context.Context, config *config.Config, stream jetstream.Stream, logger *zap.Logger, kind ids.IdKind) jetstream.MessagesContext { + logger.Debug("Starting consumer", zap.String("stream", config.Stream), zap.String("kind", string(kind))) + + consumer, err := stream.Consumer(ctx, config.Stream+"-"+string(kind)) + if err != nil { + panic(err) + } + + iter, err := consumer.Messages(jetstream.PullMaxMessages(10), jetstream.WithMessagesErrOnMissingHeartbeat(false)) + if err != nil { + panic(err) + } + + return iter +} + func BuildConsumer(stream jetstream.Stream, ctx context.Context, config *config.Config, kind ids.IdKind, logger *zap.Logger, js jetstream.JetStream, rm *auth.ResourceManager) { logger.Debug("Creating consumer", zap.String("stream", config.Stream), zap.String("kind", string(kind))) @@ -64,7 +81,7 @@ func BuildConsumer(stream jetstream.Stream, ctx context.Context, config *config. return } - ctx := getCorrelationId(ctx, nil, &headers) + ctx := GetCorrelationId(ctx, nil, &headers) // spawn a thread to process the message, but rate limit go func() { @@ -93,20 +110,267 @@ func BuildConsumer(stream jetstream.Stream, ctx context.Context, config *config. }() } +func getStateId(msg jetstream.Msg) *ids.StateId { + return ids.ParseStateId(msg.Headers().Get(string(glue.HeaderStateId))) +} + +func lockStateId(ctx context.Context, id *ids.StateId, js jetstream.JetStream, logger *zap.Logger) (func() error, error) { + if id.Kind != ids.Entity { + return func() error { return nil }, nil + } + + unlocker, err := lockSubject(ctx, id.ToSubject(), js, logger) + if err != nil { + return func() error { return nil }, err + } + return unlocker, nil +} + +func getUserFromHeader(msg jetstream.Msg) (*auth.User, error) { + r := &auth.User{} + b := msg.Headers().Get(string(glue.HeaderProvenance)) + err := json.Unmarshal([]byte(b), r) + if err != nil { + return nil, err + } + return r, nil +} + +// ProcessMessage takes a message and some references and returns: +// 1. an auth context +// 2. the destination id +// 3. the state +// 4. or an error +func ProcessMessage( + ctx context.Context, + logger *zap.Logger, + msg jetstream.Msg, + rm *auth.ResourceManager, + config *config.Config, + js jetstream.JetStream, +) ([]byte, *ids.StateId, *glue.StateArray, error) { + logger.Debug("Processing message", zap.Any("msg", msg)) + + id := getStateId(msg) + unlocker, err := lockStateId(ctx, id, nil, logger) + if err != nil { + return []byte{}, nil, nil, err + } + defer unlocker() + + ctx, cancelCtx := context.WithCancel(ctx) + defer cancelCtx() + + currentUser, err := getUserFromHeader(msg) + if err != nil { + logger.Warn("Failed to unmarshal event provenance", + zap.Any("Provenance", msg.Headers().Get(string(glue.HeaderProvenance))), + zap.Error(err), + ) + currentUser = nil + } else { + ctx = auth.DecorateContextWithUser(ctx, currentUser) + } + + // retrieve the source + sourceId := ids.ParseStateId(msg.Headers().Get(string(glue.HeaderEmittedBy))) + var authContext []byte + + if config.Extensions.Authz.Enabled { + // extract the source operations + sourceOps := strings.Split(msg.Headers().Get(string(glue.HeaderSourceOps)), ",") + + // extract the target operations + targetOps := strings.Split(msg.Headers().Get(string(glue.HeaderTargetOps)), ",") + preventCreation := true + for _, op := range targetOps { + switch auth.Operation(op) { + case auth.Signal: + fallthrough + case auth.Call: + fallthrough + case auth.Lock: + fallthrough + case auth.Output: + preventCreation = false + } + } + + resource, err := rm.DiscoverResource(ctx, id, sourceId, logger, preventCreation) + if err != nil { + logger.Warn("User attempted to perform an unauthorized operation", zap.String("operation", "create"), zap.String("From", sourceId.Id), zap.String("To", id.Id), zap.String("User", string(currentUser.UserId))) + msg.Ack() + return []byte{}, nil, nil, err + } + if resource == nil { + logger.Warn("User accessed missing object", zap.Any("operation", sourceOps), zap.String("from", sourceId.Id), zap.String("to", id.Id), zap.String("user", string(currentUser.UserId))) + msg.Ack() + return []byte{}, nil, nil, nil + } + + authContext, err = rm.ToAuthContext(ctx, resource) + if err != nil { + logger.Warn("Failed to retrieve auth context", zap.Error(err)) + msg.Ack() + return []byte{}, nil, nil, err + } + + m := msg.Headers().Get(string(glue.HeaderMeta)) + var meta map[string]interface{} + if m != "[]" { + err = json.Unmarshal([]byte(m), &meta) + if err != nil { + return []byte{}, nil, nil, err + } + + switch msg.Headers().Get(string(glue.HeaderEventType)) { + case "RevokeRole": + if !resource.WantTo(auth.ShareMinus, ctx) { + logger.Warn("User attempted to perform an unauthorized operation", zap.String("operation", "revokeRole"), zap.String("From", sourceId.Id), zap.String("To", id.Id), zap.String("User", string(currentUser.UserId))) + msg.Ack() + return []byte{}, nil, nil, nil + } + role := meta["role"].(string) + + err := resource.RevokeRole(auth.Role(role), ctx) + if err != nil { + return []byte{}, nil, nil, err + } + err = resource.Update(ctx, logger) + if err != nil { + return []byte{}, nil, nil, err + } + msg.Ack() + return []byte{}, nil, nil, nil + case "RevokeUser": + if !resource.WantTo(auth.ShareMinus, ctx) { + logger.Warn("User attempted to perform an unauthorized operation", zap.String("operation", "revokeUser"), zap.String("From", sourceId.Id), zap.String("To", id.Id), zap.String("User", string(currentUser.UserId))) + msg.Ack() + return []byte{}, nil, nil, nil + } + user := meta["userId"].(string) + err := resource.RevokeUser(auth.UserId(user), ctx) + if err != nil { + return []byte{}, nil, nil, err + } + err = resource.Update(ctx, logger) + if err != nil { + return []byte{}, nil, nil, err + } + msg.Ack() + return []byte{}, nil, nil, nil + case "ShareWithRole": + if !resource.WantTo(auth.SharePlus, ctx) { + logger.Warn("User attempted to perform an unauthorized operation", zap.String("operation", "shareWithRole"), zap.String("From", sourceId.Id), zap.String("To", id.Id), zap.String("User", string(currentUser.UserId))) + msg.Ack() + return []byte{}, nil, nil, nil + } + role := meta["role"].(auth.Role) + operations := meta["allowedOperations"].([]auth.Operation) + + for _, op := range operations { + err := resource.GrantRole(role, op, ctx) + if err != nil { + return []byte{}, nil, nil, err + } + } + err = resource.Update(ctx, logger) + if err != nil { + return []byte{}, nil, nil, err + } + msg.Ack() + return []byte{}, nil, nil, nil + case "ShareWithUser": + if !resource.WantTo(auth.SharePlus, ctx) { + logger.Warn("User attempted to perform an unauthorized operation", zap.String("operation", "shareWithUser"), zap.String("From", sourceId.Id), zap.String("To", id.Id), zap.String("User", string(currentUser.UserId))) + msg.Ack() + return []byte{}, nil, nil, nil + } + role := meta["userId"].(auth.UserId) + operations := meta["allowedOperations"].([]auth.Operation) + + for _, op := range operations { + err := resource.GrantUser(role, op, ctx) + if err != nil { + return []byte{}, nil, nil, err + } + } + err = resource.Update(ctx, logger) + if err != nil { + return []byte{}, nil, nil, err + } + msg.Ack() + return []byte{}, nil, nil, nil + case "ShareOwnership": + if !resource.WantTo(auth.Owner, ctx) { + logger.Warn("User attempted to perform an unauthorized operation", zap.String("operation", "shareOwnership"), zap.String("From", sourceId.Id), zap.String("To", id.Id), zap.String("User", string(currentUser.UserId))) + msg.Ack() + return []byte{}, nil, nil, nil + } + userId := meta["userId"].(auth.UserId) + user := ctx.Value(appcontext.CurrentUserKey).(*auth.User) + err := resource.ShareOwnership(userId, user, true) + if err != nil { + return []byte{}, nil, nil, err + } + err = resource.Update(ctx, logger) + if err != nil { + return []byte{}, nil, nil, err + } + msg.Ack() + return []byte{}, nil, nil, nil + case "GiveOwnership": + if !resource.WantTo(auth.Owner, ctx) { + logger.Warn("User attempted to perform an unauthorized operation", zap.String("operation", "giveOwnership"), zap.String("From", sourceId.Id), zap.String("To", id.Id), zap.String("User", string(currentUser.UserId))) + msg.Ack() + return []byte{}, nil, nil, nil + } + userId := meta["userId"].(auth.UserId) + user := ctx.Value(appcontext.CurrentUserKey).(*auth.User) + err := resource.ShareOwnership(userId, user, true) + if err != nil { + return []byte{}, nil, nil, err + } + err = resource.Update(ctx, logger) + if err != nil { + return []byte{}, nil, nil, err + } + msg.Ack() + return []byte{}, nil, nil, nil + } + } + + for _, op := range targetOps { + if !resource.WantTo(auth.Operation(op), ctx) { + logger.Warn("User attempted to perform an unauthorized operation", zap.String("operation", op), zap.String("From", sourceId.Id), zap.String("To", id.Id), zap.String("User", string(currentUser.UserId))) + msg.Ack() + return []byte{}, nil, nil, nil + } + } + } + + state, err := glue.GetStateArray(id, js, ctx, logger) + if err != nil { + logger.Warn("Failed to retrieve state", zap.Error(err)) + msg.Ack() + return []byte{}, nil, nil, err + } + + return authContext, id, state, nil +} + // processMsg is responsible for processing a message received from JetStream. // It takes a logger, msg, and JetStream as parameters. Do not panic! func processMsg(ctx context.Context, logger *zap.Logger, msg jetstream.Msg, js jetstream.JetStream, config *config.Config, rm *auth.ResourceManager) error { logger.Debug("Received message", zap.Any("msg", msg)) // lock the Subject, if it is a lockable Subject - id := ids.ParseStateId(msg.Headers().Get(string(glue.HeaderStateId))) - if id.Kind == ids.Entity { - unlocker, err := lockSubject(ctx, id.ToSubject(), js, logger) - if err != nil { - return err - } - defer unlocker() + id := getStateId(msg) + unlocker, err := lockStateId(ctx, id, nil, logger) + if err != nil { + return err } + defer unlocker() ctx, cancelCtx := context.WithCancel(ctx) defer cancelCtx() @@ -114,7 +378,7 @@ func processMsg(ctx context.Context, logger *zap.Logger, msg jetstream.Msg, js j // configure the current user currentUser := &auth.User{} b := msg.Headers().Get(string(glue.HeaderProvenance)) - err := json.Unmarshal([]byte(b), currentUser) + err = json.Unmarshal([]byte(b), currentUser) if err != nil { logger.Warn("Failed to unmarshal event provenance", zap.Any("Provenance", msg.Headers().Get(string(glue.HeaderProvenance))), diff --git a/cli/test.php b/cli/test.php index 0bcd5e8b..95dd5a95 100644 --- a/cli/test.php +++ b/cli/test.php @@ -1,10 +1,26 @@ 'bob', 'roles' => ['admin']], [], 'activity:bullshit'); -try { - echo emit_event([]); -} catch (Throwable $e) { - echo "\nFailed: $e"; -} +$user = new Provenance('rob', ['admin']); +$user = Serializer::serialize($user); + +$event = WithEntity::forInstance( + StateId::fromEntityId(EntityId('test', 'test')), + RaiseEvent::forTimer('ident') +); +$event = new EventDescription($event); + +echo emit_event($user, $event->toArray(), StateId::fromEntityId(EntityId('test', 'test'))); + +// echo emit_event(['userId' => 'bob', 'roles' => ['admin']], [], 'activity:bullshit'); diff --git a/src/Events/EventDescription.php b/src/Events/EventDescription.php index 3685439e..8a13144f 100644 --- a/src/Events/EventDescription.php +++ b/src/Events/EventDescription.php @@ -29,18 +29,23 @@ use Bottledcode\DurablePhp\Events\Shares\Operation; use Bottledcode\DurablePhp\State\Ids\StateId; use Bottledcode\DurablePhp\State\Serializer; +use Crell\Serde\Attributes\Field; use DateTimeImmutable; use JsonException; use ReflectionClass; readonly class EventDescription { + #[Field(default: null, omitIfNull: true)] public ?StateId $replyTo; + #[Field(serializedName: 'scheduleAt', default: null, omitIfNull: true)] public ?DateTimeImmutable $scheduledAt; + #[Field(default: null, omitIfNull: true)] public ?StateId $destination; + #[Field(default: null, omitIfNull: true)] public ?StateId $from; public string $eventId; @@ -180,6 +185,11 @@ public static function fromJson(string $json): EventDescription } public function toStream(): string + { + return json_encode($this->toArray(), JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES); + } + + public function toArray(): array { $serialized = Serializer::serialize($this->event); @@ -189,7 +199,7 @@ function_exists('igbinary_serialize') ? igbinary_serialize($serialized) : serial $event = base64_encode($serialized); - return json_encode([ + return [ 'destination' => $this->destination?->id ?? null, 'replyTo' => $this->replyTo?->id ?? '', 'scheduleAt' => $this->scheduledAt?->format(DATE_ATOM) ?? gmdate(DATE_ATOM, time() - 30), @@ -200,7 +210,7 @@ function_exists('igbinary_serialize') ? igbinary_serialize($serialized) : serial 'targetOps' => implode(',', array_map(static fn($x) => $x->value, $this->targetOperations)), 'meta' => json_encode($this->meta ?? [], JSON_THROW_ON_ERROR), 'event' => $event, - ], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES); + ]; } /** diff --git a/src/State/Ids/StateId.php b/src/State/Ids/StateId.php index 4cde9555..4f0d88d9 100644 --- a/src/State/Ids/StateId.php +++ b/src/State/Ids/StateId.php @@ -31,6 +31,7 @@ use Bottledcode\DurablePhp\State\OrchestrationInstance; use Bottledcode\DurablePhp\State\StateInterface; use Crell\Serde\Attributes\ClassNameTypeMap; +use Crell\Serde\Attributes\Field; use Exception; use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidInterface; @@ -43,7 +44,8 @@ #[ClassNameTypeMap('__type')] readonly class StateId extends Record implements Stringable { - public protected(set) string $id; + #[Field(flatten: true)] + public string $id; public static function fromState(StateInterface $state): self { From e650e83cbb746ba384ae0bbe8c1c150425212c0f Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Thu, 7 Aug 2025 18:42:51 +0200 Subject: [PATCH 05/19] eject from framework Signed-off-by: Robert Landers --- cli/Makefile | 9 +- cli/ext/build/README.md | 37 +++ cli/ext/build/ext.c | 283 +++++++++++++++++++++ cli/ext/{ => build}/ext.go | 489 +++++++++++++++++++++++------------- cli/ext/build/ext.h | 11 + cli/ext/build/ext.stub.php | 41 +++ cli/ext/build/ext_arginfo.h | 98 ++++++++ 7 files changed, 786 insertions(+), 182 deletions(-) create mode 100644 cli/ext/build/README.md create mode 100644 cli/ext/build/ext.c rename cli/ext/{ => build}/ext.go (76%) create mode 100644 cli/ext/build/ext.h create mode 100644 cli/ext/build/ext.stub.php create mode 100644 cli/ext/build/ext_arginfo.h diff --git a/cli/Makefile b/cli/Makefile index ef9e78a5..e3c5c49e 100644 --- a/cli/Makefile +++ b/cli/Makefile @@ -13,7 +13,7 @@ endif LOCAL_MODULE := /home/withinboredom/code/durable-php/cli # Targets -frankenphp: ext/build/ext.go +frankenphp: ext/build/ext.go ext/build/ext_arginfo.h CGO_ENABLED=1 \ XCADDY_GO_BUILD_FLAGS="$(XCADDY_FLAGS)" \ CGO_CFLAGS="$(PHP_INCLUDES)" \ @@ -23,8 +23,5 @@ frankenphp: ext/build/ext.go --with github.com/dunglas/frankenphp/caddy \ --with github.com/bottledcode/durable-php/cli=$(LOCAL_MODULE) -ext/build/ext.go: ext/ext.go - GEN_STUB_SCRIPT=/home/withinboredom/code/php-src/build/gen_stub.php \ - frankenphp extension-init ext/ext.go - -.PHONY: frankenphp +ext/build/ext_arginfo.h: ext/build/ext.stub.php + /home/withinboredom/code/php-src/build/gen_stub.php ext/build/ext.stub.php ext/build \ No newline at end of file diff --git a/cli/ext/build/README.md b/cli/ext/build/README.md new file mode 100644 index 00000000..93804e1b --- /dev/null +++ b/cli/ext/build/README.md @@ -0,0 +1,37 @@ +# ext Extension + +Auto-generated PHP extension from Go code. + +## Functions + +### emit_event + +```php +emit_event(?array $userContext, array $event, string $from): int +``` + +**Parameters:** + +- `userContext` (array) (nullable) +- `event` (array) +- `from` (string) + +**Returns:** int + +## Classes + +### Worker + +**Properties:** + +- `kind`: mixed +- `started`: bool +- `consumer`: mixed (nullable) +- `activeId`: mixed (nullable) +- `state`: mixed (nullable) +- `pendingEvents`: array +- `authContext`: array +- `currentCtx`: mixed +- `currentMsg`: mixed + + diff --git a/cli/ext/build/ext.c b/cli/ext/build/ext.c new file mode 100644 index 00000000..165e1b6a --- /dev/null +++ b/cli/ext/build/ext.c @@ -0,0 +1,283 @@ +#include +#include +#include +#include +#include + +#include "ext.h" +#include "ext_arginfo.h" +#include "_cgo_export.h" + +#define VALIDATE_GO_HANDLE(intern) \ + do { \ + if ((intern)->go_handle == 0) { \ + zend_throw_error(NULL, "Go object not found in registry"); \ + RETURN_THROWS(); \ + } \ + } while (0) + +static zend_object_handlers object_handlers_ext; + +typedef struct { + uintptr_t go_handle; + zend_object std; /* This must be the last field in the structure: the property store starts at this offset */ +} ext_object; + +static inline ext_object *ext_object_from_obj(zend_object *obj) { + return (ext_object*)((char*)(obj) - offsetof(ext_object, std)); +} + +static zend_object *ext_create_object(zend_class_entry *ce) { + ext_object *intern = ecalloc(1, sizeof(ext_object) + zend_object_properties_size(ce)); + + zend_object_std_init(&intern->std, ce); + object_properties_init(&intern->std, ce); + + intern->std.handlers = &object_handlers_ext; + intern->go_handle = 0; /* will be set in __construct */ + + return &intern->std; +} + +static void ext_free_object(zend_object *object) { + ext_object *intern = ext_object_from_obj(object); + + if (intern->go_handle != 0) { + removeGoObject(intern->go_handle); + } + + zend_object_std_dtor(&intern->std); +} + +void init_object_handlers() { + memcpy(&object_handlers_ext, &std_object_handlers, sizeof(zend_object_handlers)); + object_handlers_ext.free_obj = ext_free_object; + object_handlers_ext.clone_obj = NULL; + object_handlers_ext.offset = offsetof(ext_object, std); +} + +static zend_class_entry *Worker_ce = NULL; + +PHP_METHOD(Bottledcode_DurablePhp_Ext_Worker, __construct) { + ZEND_PARSE_PARAMETERS_NONE(); + + ext_object *intern = ext_object_from_obj(Z_OBJ_P(ZEND_THIS)); + + /* Constructor is called more than once, make it no-op */ + if (intern->go_handle != 0) { + return; + } + + intern->go_handle = create_Worker_object(); +} + + +PHP_METHOD(Bottledcode_DurablePhp_Ext_Worker, startEventLoop) { + ext_object *intern = ext_object_from_obj(Z_OBJ_P(ZEND_THIS)); + + VALIDATE_GO_HANDLE(intern); + zend_string *kind = NULL; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(kind) + ZEND_PARSE_PARAMETERS_END(); + + startEventLoop_wrapper(intern->go_handle, kind); +} + +PHP_METHOD(Bottledcode_DurablePhp_Ext_Worker, drainEventLoop) { + ext_object *intern = ext_object_from_obj(Z_OBJ_P(ZEND_THIS)); + + VALIDATE_GO_HANDLE(intern); + ZEND_PARSE_PARAMETERS_NONE(); + + drainEventLoop_wrapper(intern->go_handle); +} + +PHP_METHOD(Bottledcode_DurablePhp_Ext_Worker, __destruct) { + ext_object *intern = ext_object_from_obj(Z_OBJ_P(ZEND_THIS)); + + VALIDATE_GO_HANDLE(intern); + ZEND_PARSE_PARAMETERS_NONE(); + + __destruct_wrapper(intern->go_handle); +} + +PHP_METHOD(Bottledcode_DurablePhp_Ext_Worker, getNextEvent) { + ext_object *intern = ext_object_from_obj(Z_OBJ_P(ZEND_THIS)); + + VALIDATE_GO_HANDLE(intern); + ZEND_PARSE_PARAMETERS_NONE(); + + zend_string* result = getNextEvent_wrapper(intern->go_handle); + RETURN_STR(result); +} + +PHP_METHOD(Bottledcode_DurablePhp_Ext_Worker, queryState) { + ext_object *intern = ext_object_from_obj(Z_OBJ_P(ZEND_THIS)); + + VALIDATE_GO_HANDLE(intern); + zend_string *stateId = NULL; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(stateId) + ZEND_PARSE_PARAMETERS_END(); + + void* result = queryState_wrapper(intern->go_handle, stateId); + if (result != NULL) { + HashTable *ht = (HashTable*)result; + RETURN_ARR(ht); + } else { + RETURN_NULL(); + } +} + +PHP_METHOD(Bottledcode_DurablePhp_Ext_Worker, getUser) { + ext_object *intern = ext_object_from_obj(Z_OBJ_P(ZEND_THIS)); + + VALIDATE_GO_HANDLE(intern); + ZEND_PARSE_PARAMETERS_NONE(); + + void* result = getUser_wrapper(intern->go_handle); + if (result != NULL) { + HashTable *ht = (HashTable*)result; + RETURN_ARR(ht); + } else { + RETURN_NULL(); + } +} + +PHP_METHOD(Bottledcode_DurablePhp_Ext_Worker, getSource) { + ext_object *intern = ext_object_from_obj(Z_OBJ_P(ZEND_THIS)); + + VALIDATE_GO_HANDLE(intern); + ZEND_PARSE_PARAMETERS_NONE(); + + zend_string* result = getSource_wrapper(intern->go_handle); + RETURN_STR(result); +} + +PHP_METHOD(Bottledcode_DurablePhp_Ext_Worker, getCurrentId) { + ext_object *intern = ext_object_from_obj(Z_OBJ_P(ZEND_THIS)); + + VALIDATE_GO_HANDLE(intern); + ZEND_PARSE_PARAMETERS_NONE(); + + zend_string* result = getCurrentId_wrapper(intern->go_handle); + RETURN_STR(result); +} + +PHP_METHOD(Bottledcode_DurablePhp_Ext_Worker, getCorrelationId) { + ext_object *intern = ext_object_from_obj(Z_OBJ_P(ZEND_THIS)); + + VALIDATE_GO_HANDLE(intern); + ZEND_PARSE_PARAMETERS_NONE(); + + zend_string* result = getCorrelationId_wrapper(intern->go_handle); + RETURN_STR(result); +} + +PHP_METHOD(Bottledcode_DurablePhp_Ext_Worker, getState) { + ext_object *intern = ext_object_from_obj(Z_OBJ_P(ZEND_THIS)); + + VALIDATE_GO_HANDLE(intern); + ZEND_PARSE_PARAMETERS_NONE(); + + void* result = getState_wrapper(intern->go_handle); + if (result != NULL) { + HashTable *ht = (HashTable*)result; + RETURN_ARR(ht); + } else { + RETURN_NULL(); + } +} + +PHP_METHOD(Bottledcode_DurablePhp_Ext_Worker, updateState) { + ext_object *intern = ext_object_from_obj(Z_OBJ_P(ZEND_THIS)); + + VALIDATE_GO_HANDLE(intern); + zval *state = NULL; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ARRAY(state) + ZEND_PARSE_PARAMETERS_END(); + + updateState_wrapper(intern->go_handle, state); +} + +PHP_METHOD(Bottledcode_DurablePhp_Ext_Worker, emitEvent) { + ext_object *intern = ext_object_from_obj(Z_OBJ_P(ZEND_THIS)); + + VALIDATE_GO_HANDLE(intern); + zval *eventDescription = NULL; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ARRAY(eventDescription) + ZEND_PARSE_PARAMETERS_END(); + + emitEvent_wrapper(intern->go_handle, eventDescription); +} + +PHP_METHOD(Bottledcode_DurablePhp_Ext_Worker, delete) { + ext_object *intern = ext_object_from_obj(Z_OBJ_P(ZEND_THIS)); + + VALIDATE_GO_HANDLE(intern); + ZEND_PARSE_PARAMETERS_NONE(); + + delete_wrapper(intern->go_handle); +} + +void register_all_classes() { + init_object_handlers(); + Worker_ce = register_class_Bottledcode_DurablePhp_Ext_Worker(); + if (!Worker_ce) { + php_error_docref(NULL, E_ERROR, "Failed to register class Worker"); + return; + } + Worker_ce->create_object = ext_create_object; +} + +PHP_MINIT_FUNCTION(ext) { + register_all_classes(); + + + go_init_module(); + + + return SUCCESS; +} + + + +PHP_MSHUTDOWN_FUNCTION(ext) { + go_shutdown_module(); + return SUCCESS; +} + + + +zend_module_entry ext_module_entry = {STANDARD_MODULE_HEADER, + "ext", + ext_functions, /* Functions */ + PHP_MINIT(ext), /* MINIT */ + PHP_MSHUTDOWN(ext), /* MSHUTDOWN */ + NULL, /* RINIT */ + NULL, /* RSHUTDOWN */ + NULL, /* MINFO */ + "1.0.0", /* Version */ + STANDARD_MODULE_PROPERTIES}; + +PHP_FUNCTION(Bottledcode_DurablePhp_Ext_emit_event) +{ + zval *userContext = NULL; + zval *event = NULL; + zend_string *from = NULL; + ZEND_PARSE_PARAMETERS_START(3, 3) + Z_PARAM_ARRAY_OR_NULL(userContext) + Z_PARAM_ARRAY(event) + Z_PARAM_STR(from) + ZEND_PARSE_PARAMETERS_END(); + long result = emit_event(userContext, event, from); + RETURN_LONG(result); +} + diff --git a/cli/ext/ext.go b/cli/ext/build/ext.go similarity index 76% rename from cli/ext/ext.go rename to cli/ext/build/ext.go index c2415877..b9cab237 100644 --- a/cli/ext/ext.go +++ b/cli/ext/build/ext.go @@ -1,48 +1,63 @@ -package ext +package build +/* +#include +#include "ext.h" +*/ import "C" +import "runtime/cgo" +import "unsafe" +import "github.com/dunglas/frankenphp" +import "context" +import "encoding/json" +import "errors" +import "os" +import "strings" +import "sync" +import "time" +import "github.com/bottledcode/durable-php/cli/appcontext" +import "github.com/bottledcode/durable-php/cli/auth" +import "github.com/bottledcode/durable-php/cli/config" +import "github.com/bottledcode/durable-php/cli/ext/helpers" +import "github.com/bottledcode/durable-php/cli/glue" +import "github.com/bottledcode/durable-php/cli/ids" +import "github.com/bottledcode/durable-php/cli/lib" +import "github.com/nats-io/nats-server/v2/server" +import "github.com/nats-io/nats-server/v2/test" +import "github.com/nats-io/nats.go" +import "github.com/nats-io/nats.go/jetstream" +import "go.uber.org/zap" + +func init() { + frankenphp.RegisterExtension(unsafe.Pointer(&C.ext_module_entry)) +} -import ( - "context" - "encoding/json" - "errors" - "os" - "strings" - "sync" - "time" - "unsafe" - - "github.com/bottledcode/durable-php/cli/appcontext" - "github.com/bottledcode/durable-php/cli/auth" - "github.com/bottledcode/durable-php/cli/config" - "github.com/bottledcode/durable-php/cli/ext/helpers" - "github.com/bottledcode/durable-php/cli/glue" - "github.com/bottledcode/durable-php/cli/ids" - "github.com/bottledcode/durable-php/cli/lib" - "github.com/dunglas/frankenphp" - "github.com/nats-io/nats-server/v2/server" - "github.com/nats-io/nats-server/v2/test" - "github.com/nats-io/nats.go" - "github.com/nats-io/nats.go/jetstream" - "go.uber.org/zap" -) - -/** -This is a php extension that operates as a client for durable php -*/ +func Authorize(ctx context.Context, ev *glue.EventMessage, from *ids.StateId, preventCreation bool, operation auth.Operation) (bool, error) { + if !helpers.Config.Extensions.Authz.Enabled { + return true, nil + } -// export_php:namespace Bottledcode\DurablePhp\Ext + rm := auth.GetResourceManager(ctx, helpers.Js) + r, err := rm.DiscoverResource(ctx, ids.ParseStateId(ev.Destination), from, helpers.Logger, preventCreation) + if err != nil { + err = errors.Join(errors.New("user is not authorised"), err) + helpers.ThrowPHPException(err.Error()) + return false, err + } + if r == nil { + return false, nil + } -// export_php:module shutdown -func go_shutdown_module() { - if helpers.NatServer != nil { - helpers.NatServer.Shutdown() + if !r.WantTo(operation, ctx) { + err = errors.New("user is not authorised") + helpers.ThrowPHPException(err.Error()) + return false, err } - // remove nats state directory - os.RemoveAll(helpers.NatsState) + + return true, nil } -// export_php:module init +//export go_init_module func go_init_module() { cfg, err := config.GetProjectConfig() if err != nil { @@ -317,74 +332,118 @@ func go_init_module() { } } -// Authorize the user to access the given event destination. -// (false, nil) means not found, while (true, nil) means they're allowed. -// Any error means the user is not authorised. -func Authorize(ctx context.Context, ev *glue.EventMessage, from *ids.StateId, preventCreation bool, operation auth.Operation) (bool, error) { - if !helpers.Config.Extensions.Authz.Enabled { - return true, nil +//export go_shutdown_module +func go_shutdown_module() { + if helpers.NatServer != nil { + helpers.NatServer.Shutdown() + } + // remove nats state directory + os.RemoveAll(helpers.NatsState) +} + +//export emit_event +func emit_event(userContext *C.zval, event *C.zval, fromStr *C.zend_string) int64 { + + userVal := frankenphp.GoArray(unsafe.Pointer(userContext)) + + var user *auth.User + ctx, cancel := context.WithCancel(helpers.Ctx) + defer cancel() + + if userVal != nil { + userArr := frankenphp.GoArray(unsafe.Pointer(userVal)) + user = helpers.GetUserContext(userArr) + if user.UserId == "" || len(user.Roles) == 0 { + helpers.ThrowPHPException("User context is missing userId or roles") + return 0 + } + ctx = auth.DecorateContextWithUser(ctx, user) } - rm := auth.GetResourceManager(ctx, helpers.Js) - r, err := rm.DiscoverResource(ctx, ids.ParseStateId(ev.Destination), from, helpers.Logger, preventCreation) + from := ids.ParseStateId(frankenphp.GoString(unsafe.Pointer(fromStr))) + + eventArr := frankenphp.GoArray(unsafe.Pointer(event)) + ev, err := helpers.ParseEvent(eventArr) if err != nil { - err = errors.Join(errors.New("user is not authorised"), err) helpers.ThrowPHPException(err.Error()) - return false, err + return 0 } - if r == nil { - return false, nil + + operation := auth.Operation(ev.TargetOps) + preventCreation := true + switch operation { + case auth.Lock: + fallthrough + case auth.Call: + fallthrough + case auth.Signal: + fallthrough + case auth.Output: + preventCreation = false } - if !r.WantTo(operation, ctx) { - err = errors.New("user is not authorised") + authd, err := Authorize(ctx, ev, from, preventCreation, operation) + if err != nil { helpers.ThrowPHPException(err.Error()) - return false, err + return 0 } - - return true, nil -} - -// export_php:method Worker::startEventLoop(): void -func (w *Worker) startEventLoop(kindStr *C.zend_string) { - if w.started { - helpers.ThrowPHPException("Event loop already running") - return + if !authd { + helpers.ThrowPHPException("Resource not found") + return 0 + } + replyTo := "" + if ev.ReplyTo != "" { + replyTo = ids.ParseStateId(ev.ReplyTo).ToSubject().String() } - ctx, done := context.WithCancel(helpers.Ctx) + splitType := strings.Split(ev.EventType, "\\") + eventType := splitType[len(splitType)-1] - c := &helpers.Consumer{ - Context: ctx, - Done: done, + destinationId := ids.ParseStateId(ev.Destination) + + now, err := time.Now().MarshalText() + if err != nil { + helpers.ThrowPHPException(err.Error()) + return 0 } - stream, err := helpers.Js.Stream(ctx, helpers.Config.Stream) + userJson, err := json.Marshal(user) if err != nil { helpers.ThrowPHPException(err.Error()) - return + return 0 } - c.Msg = lib.StartConsumer(ctx, helpers.Config, stream, helpers.Logger, w.kind) - w.consumer = c -} + header := make(nats.Header) + header.Add(string(glue.HeaderStateId), destinationId.String()) + header.Add(string(glue.HeaderEventType), eventType) + header.Add(string(glue.HeaderTargetType), ev.TargetType) + header.Add(string(glue.HeaderEmittedAt), string(now)) + header.Add(string(glue.HeaderProvenance), string(userJson)) + header.Add(string(glue.HeaderTargetOps), ev.TargetOps) + header.Add(string(glue.HeaderSourceOps), ev.SourceOps) + header.Add(string(glue.HeaderMeta), ev.Meta) + header.Add(string(glue.HeaderEmittedBy), from.String()) -// export_php:method Worker::drainEventLoop(): void -func (w *Worker) drainEventLoop() { - if !w.started { - return + msg := &nats.Msg{ + Subject: destinationId.ToSubject().String(), + Reply: replyTo, + Header: header, + Data: []byte(ev.Event), } - w.consumer.Msg.Drain() - w.started = false -} -// export_php:method Worker::__destruct(): void -func (w *Worker) __destruct() { - w.consumer.Msg.Stop() - w.consumer.Done() + if ev.ScheduleAt.After(time.Now()) { + msg.Header.Add(string(glue.HeaderDelay), ev.ScheduleAt.Format(time.RFC3339)) + } + + ack, err := helpers.Js.PublishMsg(ctx, msg) + if err != nil { + helpers.ThrowPHPException(err.Error()) + return 0 + } + return int64(ack.Sequence) + } -// export_php:class Worker type Worker struct { kind ids.IdKind started bool @@ -397,8 +456,30 @@ type Worker struct { currentMsg jetstream.Msg } -// export_php:method Worker::__construct(string $kind): void -func (w *Worker) __construct(kindStr *C.zend_string) { +//export registerGoObject +func registerGoObject(obj interface{}) C.uintptr_t { + handle := cgo.NewHandle(obj) + return C.uintptr_t(handle) +} + +//export getGoObject +func getGoObject(handle C.uintptr_t) interface{} { + h := cgo.Handle(handle) + return h.Value() +} + +//export removeGoObject +func removeGoObject(handle C.uintptr_t) { + h := cgo.Handle(handle) + h.Delete() +} + +//export create_Worker_object +func create_Worker_object() C.uintptr_t { + obj := &Worker{} + return registerGoObject(obj) +} +func (w *Worker) startEventLoop(kindStr *C.zend_string) { kind := ids.IdKind(frankenphp.GoString(unsafe.Pointer(kindStr))) switch kind { @@ -410,9 +491,42 @@ func (w *Worker) __construct(kindStr *C.zend_string) { return } w.kind = kind + + if w.started { + helpers.ThrowPHPException("Event loop already running") + return + } + + ctx, done := context.WithCancel(helpers.Ctx) + + c := &helpers.Consumer{ + Context: ctx, + Done: done, + } + + stream, err := helpers.Js.Stream(ctx, helpers.Config.Stream) + if err != nil { + helpers.ThrowPHPException(err.Error()) + return + } + + c.Msg = lib.StartConsumer(ctx, helpers.Config, stream, helpers.Logger, w.kind) + w.consumer = c +} + +func (w *Worker) drainEventLoop() { + if !w.started { + return + } + w.consumer.Msg.Drain() + w.started = false +} + +func (w *Worker) __destruct() { + w.consumer.Msg.Stop() + w.consumer.Done() } -// export_php:method Worker::getNextEvent(): ?string func (w *Worker) getNextEvent() unsafe.Pointer { c := w.consumer @@ -485,9 +599,8 @@ func (w *Worker) getNextEvent() unsafe.Pointer { return frankenphp.PHPString(string(msg.Data()), false) } -// export_php:method Worker::queryState(string $stateId): array -func (w *Worker) queryState(idStr unsafe.Pointer) unsafe.Pointer { - id := ids.ParseStateId(frankenphp.GoString(idStr)) +func (w *Worker) queryState(idStr *C.zend_string) unsafe.Pointer { + id := ids.ParseStateId(frankenphp.GoString(unsafe.Pointer(idStr))) state, err := glue.GetStateArray(id, helpers.Js, w.currentCtx, helpers.Logger) if err != nil { helpers.ThrowPHPException(err.Error()) @@ -496,7 +609,6 @@ func (w *Worker) queryState(idStr unsafe.Pointer) unsafe.Pointer { return frankenphp.PHPArray(state.Data.Array) } -// export_php:method Worker::getUser(): ?array func (w *Worker) getUser() unsafe.Pointer { if provenance, ok := w.currentCtx.Value(appcontext.CurrentUserKey).(*auth.User); ok { ret := &glue.Array{} @@ -513,136 +625,161 @@ func (w *Worker) getUser() unsafe.Pointer { return nil } -// export_php:method Worker::getSource(): string func (w *Worker) getSource() unsafe.Pointer { sourceId := ids.ParseStateId(w.currentMsg.Headers().Get(string(glue.HeaderEmittedBy))) return frankenphp.PHPString(sourceId.String(), false) } -// export_php:method Worker::getCurrentId(): string func (w *Worker) getCurrentId() unsafe.Pointer { return frankenphp.PHPString(w.activeId.String(), false) } -// export_php:method Worker::getCorrelationId(): string func (w *Worker) getCorrelationId() unsafe.Pointer { return frankenphp.PHPString(w.currentCtx.Value("cid").(string), false) } -// export_php:method Worker::getState(): ?array func (w *Worker) getState() unsafe.Pointer { return frankenphp.PHPArray(w.state.Data.Array) } -// export_php:method Worker::updateState(array $state): void -func (w *Worker) updateState(state unsafe.Pointer) { - arr := frankenphp.GoArray(state) +func (w *Worker) updateState(state *C.zval) { + arr := frankenphp.GoArray(unsafe.Pointer(state)) w.state.Data.Array = arr } -// export_php:method Worker::emitEvent(array $eventDescription): void -func (w *Worker) emitEvent(event unsafe.Pointer) { - arr := frankenphp.GoArray(event) +func (w *Worker) emitEvent(event *C.zval) { + arr := frankenphp.GoArray(unsafe.Pointer(event)) w.pendingEvents = append(w.pendingEvents, arr) } -func (w *Worker) deleteState() {} +func (w *Worker) delete() {} -// export_php:function emit_event(?array $userContext, array $event, string $from): int -func emit_event(userVal *C.zval, event *C.zval, fromStr *C.zend_string) int64 { - var user *auth.User - ctx, cancel := context.WithCancel(helpers.Ctx) - defer cancel() - - if userVal != nil { - userArr := frankenphp.GoArray(unsafe.Pointer(userVal)) - user = helpers.GetUserContext(userArr) - if user.UserId == "" || len(user.Roles) == 0 { - helpers.ThrowPHPException("User context is missing userId or roles") - return 0 - } - ctx = auth.DecorateContextWithUser(ctx, user) +//export startEventLoop_wrapper +func startEventLoop_wrapper(handle C.uintptr_t, kind *C.zend_string) { + obj := getGoObject(handle) + if obj == nil { + return } + structObj := obj.(*Worker) + structObj.startEventLoop(kind) +} - from := ids.ParseStateId(frankenphp.GoString(unsafe.Pointer(fromStr))) - - eventArr := frankenphp.GoArray(unsafe.Pointer(event)) - ev, err := helpers.ParseEvent(eventArr) - if err != nil { - helpers.ThrowPHPException(err.Error()) - return 0 +//export drainEventLoop_wrapper +func drainEventLoop_wrapper(handle C.uintptr_t) { + obj := getGoObject(handle) + if obj == nil { + return } + structObj := obj.(*Worker) + structObj.drainEventLoop() +} - operation := auth.Operation(ev.TargetOps) - preventCreation := true - switch operation { - case auth.Lock: - fallthrough - case auth.Call: - fallthrough - case auth.Signal: - fallthrough - case auth.Output: - preventCreation = false +//export __destruct_wrapper +func __destruct_wrapper(handle C.uintptr_t) { + obj := getGoObject(handle) + if obj == nil { + return } + structObj := obj.(*Worker) + structObj.__destruct() +} - authd, err := Authorize(ctx, ev, from, preventCreation, operation) - if err != nil { - helpers.ThrowPHPException(err.Error()) - return 0 - } - if !authd { - helpers.ThrowPHPException("Resource not found") - return 0 +//export getNextEvent_wrapper +func getNextEvent_wrapper(handle C.uintptr_t) unsafe.Pointer { + obj := getGoObject(handle) + if obj == nil { + return nil } - replyTo := "" - if ev.ReplyTo != "" { - replyTo = ids.ParseStateId(ev.ReplyTo).ToSubject().String() + structObj := obj.(*Worker) + return structObj.getNextEvent() +} + +//export queryState_wrapper +func queryState_wrapper(handle C.uintptr_t, stateId *C.zend_string) unsafe.Pointer { + obj := getGoObject(handle) + if obj == nil { + return nil } + structObj := obj.(*Worker) + return structObj.queryState(stateId) +} - splitType := strings.Split(ev.EventType, "\\") - eventType := splitType[len(splitType)-1] +//export getUser_wrapper +func getUser_wrapper(handle C.uintptr_t) unsafe.Pointer { + obj := getGoObject(handle) + if obj == nil { + return nil + } + structObj := obj.(*Worker) + return structObj.getUser() +} - destinationId := ids.ParseStateId(ev.Destination) +//export getSource_wrapper +func getSource_wrapper(handle C.uintptr_t) unsafe.Pointer { + obj := getGoObject(handle) + if obj == nil { + return nil + } + structObj := obj.(*Worker) + return structObj.getSource() +} - now, err := time.Now().MarshalText() - if err != nil { - helpers.ThrowPHPException(err.Error()) - return 0 +//export getCurrentId_wrapper +func getCurrentId_wrapper(handle C.uintptr_t) unsafe.Pointer { + obj := getGoObject(handle) + if obj == nil { + return nil } + structObj := obj.(*Worker) + return structObj.getCurrentId() +} - userJson, err := json.Marshal(user) - if err != nil { - helpers.ThrowPHPException(err.Error()) - return 0 +//export getCorrelationId_wrapper +func getCorrelationId_wrapper(handle C.uintptr_t) unsafe.Pointer { + obj := getGoObject(handle) + if obj == nil { + return nil } + structObj := obj.(*Worker) + return structObj.getCorrelationId() +} - header := make(nats.Header) - header.Add(string(glue.HeaderStateId), destinationId.String()) - header.Add(string(glue.HeaderEventType), eventType) - header.Add(string(glue.HeaderTargetType), ev.TargetType) - header.Add(string(glue.HeaderEmittedAt), string(now)) - header.Add(string(glue.HeaderProvenance), string(userJson)) - header.Add(string(glue.HeaderTargetOps), ev.TargetOps) - header.Add(string(glue.HeaderSourceOps), ev.SourceOps) - header.Add(string(glue.HeaderMeta), ev.Meta) - header.Add(string(glue.HeaderEmittedBy), from.String()) +//export getState_wrapper +func getState_wrapper(handle C.uintptr_t) unsafe.Pointer { + obj := getGoObject(handle) + if obj == nil { + return nil + } + structObj := obj.(*Worker) + return structObj.getState() +} - msg := &nats.Msg{ - Subject: destinationId.ToSubject().String(), - Reply: replyTo, - Header: header, - Data: []byte(ev.Event), +//export updateState_wrapper +func updateState_wrapper(handle C.uintptr_t, state *C.zval) { + obj := getGoObject(handle) + if obj == nil { + return } + structObj := obj.(*Worker) + structObj.updateState(state) +} - if ev.ScheduleAt.After(time.Now()) { - msg.Header.Add(string(glue.HeaderDelay), ev.ScheduleAt.Format(time.RFC3339)) +//export emitEvent_wrapper +func emitEvent_wrapper(handle C.uintptr_t, eventDescription *C.zval) { + obj := getGoObject(handle) + if obj == nil { + return } + structObj := obj.(*Worker) + structObj.emitEvent(eventDescription) +} - ack, err := helpers.Js.PublishMsg(ctx, msg) - if err != nil { - helpers.ThrowPHPException(err.Error()) - return 0 +//export delete_wrapper +func delete_wrapper(handle C.uintptr_t) { + obj := getGoObject(handle) + if obj == nil { + return } - return int64(ack.Sequence) + structObj := obj.(*Worker) + structObj.delete() } diff --git a/cli/ext/build/ext.h b/cli/ext/build/ext.h new file mode 100644 index 00000000..1ab81f1a --- /dev/null +++ b/cli/ext/build/ext.h @@ -0,0 +1,11 @@ +#ifndef _EXT_H +#define _EXT_H + +#include +#include + +extern zend_module_entry ext_module_entry; + + + +#endif diff --git a/cli/ext/build/ext.stub.php b/cli/ext/build/ext.stub.php new file mode 100644 index 00000000..862b549c --- /dev/null +++ b/cli/ext/build/ext.stub.php @@ -0,0 +1,41 @@ + Date: Fri, 8 Aug 2025 20:32:19 +0200 Subject: [PATCH 06/19] update build process Signed-off-by: Robert Landers --- cli/Makefile | 6 +++-- cli/ext/build/ext.go | 3 +++ cli/ext/worker/thread.go | 1 + cli/go.mod | 18 +++------------ cli/go.sum | 49 ++-------------------------------------- 5 files changed, 13 insertions(+), 64 deletions(-) create mode 100644 cli/ext/worker/thread.go diff --git a/cli/Makefile b/cli/Makefile index e3c5c49e..c73704a0 100644 --- a/cli/Makefile +++ b/cli/Makefile @@ -11,6 +11,8 @@ else endif LOCAL_MODULE := /home/withinboredom/code/durable-php/cli +GEN_STUB := /home/withinboredom/code/php-src/build/gen_stub.php +LOCAL_FRANKENPHP := /home/withinboredom/code/frankenphp/caddy # Targets frankenphp: ext/build/ext.go ext/build/ext_arginfo.h @@ -20,8 +22,8 @@ frankenphp: ext/build/ext.go ext/build/ext_arginfo.h CGO_LDFLAGS="$(PHP_LDFLAGS) $(PHP_LIBS)" \ xcaddy build \ --output frankenphp \ - --with github.com/dunglas/frankenphp/caddy \ + --with github.com/dunglas/frankenphp/caddy=$(LOCAL_FRANKENPHP) \ --with github.com/bottledcode/durable-php/cli=$(LOCAL_MODULE) ext/build/ext_arginfo.h: ext/build/ext.stub.php - /home/withinboredom/code/php-src/build/gen_stub.php ext/build/ext.stub.php ext/build \ No newline at end of file + $(GEN_STUB) ext/build/ext.stub.php ext/build \ No newline at end of file diff --git a/cli/ext/build/ext.go b/cli/ext/build/ext.go index b9cab237..103344f6 100644 --- a/cli/ext/build/ext.go +++ b/cli/ext/build/ext.go @@ -30,6 +30,9 @@ import "go.uber.org/zap" func init() { frankenphp.RegisterExtension(unsafe.Pointer(&C.ext_module_entry)) + + // initialize the workers + } func Authorize(ctx context.Context, ev *glue.EventMessage, from *ids.StateId, preventCreation bool, operation auth.Operation) (bool, error) { diff --git a/cli/ext/worker/thread.go b/cli/ext/worker/thread.go new file mode 100644 index 00000000..4df0094f --- /dev/null +++ b/cli/ext/worker/thread.go @@ -0,0 +1 @@ +package worker diff --git a/cli/go.mod b/cli/go.mod index 4970ddcd..8ebdcaac 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -4,6 +4,8 @@ go 1.24.5 require github.com/dunglas/frankenphp v1.9.0 +replace github.com/dunglas/frankenphp v1.9.0 => /home/withinboredom/code/frankenphp + require github.com/nats-io/nats.go v1.44.0 require github.com/nats-io/nats-server/v2 v2.11.7 @@ -31,12 +33,8 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/dolthub/maphash v0.1.0 // indirect github.com/gammazero/deque v1.1.0 // indirect - github.com/go-ole/go-ole v1.2.6 // indirect github.com/google/go-tpm v0.9.5 // indirect - github.com/google/gops v0.3.28 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/compress v1.18.0 // indirect - github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/maypok86/otter v1.2.4 // indirect github.com/minio/highwayhash v1.0.3 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect @@ -45,25 +43,15 @@ require ( github.com/nats-io/nuid v1.0.1 // indirect github.com/oapi-codegen/runtime v1.1.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/prometheus/client_golang v1.23.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.65.0 // indirect github.com/prometheus/procfs v0.17.0 // indirect - github.com/shirou/gopsutil/v3 v3.23.7 // indirect - github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/sony/gobreaker v1.0.0 // indirect - github.com/spf13/cobra v1.7.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/tklauser/go-sysconf v0.3.11 // indirect - github.com/tklauser/numcpus v0.6.0 // indirect - github.com/xlab/treeprint v1.2.0 // indirect - github.com/yusufpapurcu/wmi v1.2.3 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.40.0 // indirect golang.org/x/sys v0.34.0 // indirect golang.org/x/time v0.12.0 // indirect - google.golang.org/protobuf v1.36.6 // indirect + google.golang.org/protobuf v1.36.7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - rsc.io/goversion v1.2.0 // indirect ) diff --git a/cli/go.sum b/cli/go.sum index 61bb154c..904921ce 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -8,34 +8,23 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ= github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4= -github.com/dunglas/frankenphp v1.9.0 h1:tucI7uSZEmwGRGg7JxAf3wTwLrYs319mSc6fATG9z5I= -github.com/dunglas/frankenphp v1.9.0/go.mod h1:jpmWK5Nmi2LkpgL+Td0+LQWRcQ5jVOYsuT9f+L7ohDs= github.com/gammazero/deque v1.1.0 h1:OyiyReBbnEG2PP0Bnv1AASLIYvyKqIFN5xfl1t8oGLo= github.com/gammazero/deque v1.1.0/go.mod h1:JVrR+Bj1NMQbPnYclvDlvSX0nVGReLrQZ0aUMuWLctg= -github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU= github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= -github.com/google/gops v0.3.28 h1:2Xr57tqKAmQYRAfG12E+yLcoa2Y42UJo2lOrUFL9ark= -github.com/google/gops v0.3.28/go.mod h1:6f6+Nl8LcHrzJwi8+p0ii+vmBFSlB4f8cOOkTJ7sk4c= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jinzhu/copier v0.3.4 h1:mfU6jI9PtCeUjkjQ322dlff9ELjGDu975C2p/nrubVI= github.com/jinzhu/copier v0.3.4/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= @@ -47,8 +36,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/maypok86/otter v1.2.4 h1:HhW1Pq6VdJkmWwcZZq19BlEQkHtI8xgsQzBVXJU0nfc= github.com/maypok86/otter v1.2.4/go.mod h1:mKLfoI7v1HOmQMwFgX4QkRk23mX6ge3RDvjdHOWG4R4= github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q= @@ -71,8 +58,6 @@ github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQ github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= @@ -83,41 +68,17 @@ github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7D github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shirou/gopsutil/v3 v3.23.7 h1:C+fHO8hfIppoJ1WdsVm1RoI0RwXoNdfTK7yWXV0wVj4= -github.com/shirou/gopsutil/v3 v3.23.7/go.mod h1:c4gnmoRC0hQuaLqvxnx1//VXQ0Ms/X9UnJF8pddY5z4= -github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= -github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= -github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ= github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/teris-io/cli v1.0.1 h1:J6jnVHC552uqx7zT+Ux0++tIvLmJQULqxVhCid2u/Gk= github.com/teris-io/cli v1.0.1/go.mod h1:V9nVD5aZ873RU/tQXLSXO8FieVPQhQvuNohsdsKXsGw= -github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= -github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= -github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms= -github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= github.com/typesense/typesense-go v1.1.0 h1:QocehDarVXRArMIosPIdawiVFZZbnRkPJxwnAGOFkzw= github.com/typesense/typesense-go v1.1.0/go.mod h1:KcPODU7ltrcUFC/gygMTkAAfZ9M8/q6ayrdl1MnE1kI= -github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= -github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= -github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= -github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= @@ -132,10 +93,6 @@ golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= @@ -143,14 +100,12 @@ golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= +google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -rsc.io/goversion v1.2.0 h1:SPn+NLTiAG7w30IRK/DKp1BjvpWabYgxlLp/+kx5J8w= -rsc.io/goversion v1.2.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo= From 4db4fb75f99a5b403f6959249b68d519e6ff3da2 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sat, 9 Aug 2025 15:37:22 +0200 Subject: [PATCH 07/19] add worker registration Signed-off-by: Robert Landers --- cli/ext/build/ext.go | 43 ++++++++++++++++++++++++++++++++++++++-- cli/ext/worker/thread.go | 1 - 2 files changed, 41 insertions(+), 3 deletions(-) delete mode 100644 cli/ext/worker/thread.go diff --git a/cli/ext/build/ext.go b/cli/ext/build/ext.go index 103344f6..20826c7b 100644 --- a/cli/ext/build/ext.go +++ b/cli/ext/build/ext.go @@ -5,7 +5,9 @@ package build #include "ext.h" */ import "C" -import "runtime/cgo" +import ( + "runtime/cgo" +) import "unsafe" import "github.com/dunglas/frankenphp" import "context" @@ -28,11 +30,48 @@ import "github.com/nats-io/nats.go" import "github.com/nats-io/nats.go/jetstream" import "go.uber.org/zap" +type worker struct { +} + +func (w *worker) Name() string { + return "m#durable-php" +} + +func (w *worker) FileName() string { + // check if target exists + if _, err := os.Stat("src/glue/worker.php"); !os.IsNotExist(err) { + return "src/glue/worker.php" + } + + return "vendor/bottledcode/durable-php/src/glue/worker.php" +} + +func (w *worker) Env() frankenphp.PreparedEnv { + return frankenphp.PreparedEnv{} +} + +func (w *worker) GetMinThreads() int { + return 4 +} + +func (w *worker) ThreadActivatedNotification(threadId int) { +} + +func (w *worker) ThreadDrainNotification(threadId int) { +} + +func (w *worker) ThreadDeactivatedNotification(threadId int) { +} + +func (w *worker) ProvideRequest() *frankenphp.WorkerRequest { + return nil +} + func init() { frankenphp.RegisterExtension(unsafe.Pointer(&C.ext_module_entry)) // initialize the workers - + frankenphp.RegisterExternalWorker(&worker{}) } func Authorize(ctx context.Context, ev *glue.EventMessage, from *ids.StateId, preventCreation bool, operation auth.Operation) (bool, error) { diff --git a/cli/ext/worker/thread.go b/cli/ext/worker/thread.go deleted file mode 100644 index 4df0094f..00000000 --- a/cli/ext/worker/thread.go +++ /dev/null @@ -1 +0,0 @@ -package worker From 3a0475c9b86300521ac646fa6cf1e9b2c6e1a1ba Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sat, 9 Aug 2025 15:41:06 +0200 Subject: [PATCH 08/19] fixed build issues Signed-off-by: Robert Landers --- cli/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/Makefile b/cli/Makefile index c73704a0..a13d10fb 100644 --- a/cli/Makefile +++ b/cli/Makefile @@ -12,7 +12,7 @@ endif LOCAL_MODULE := /home/withinboredom/code/durable-php/cli GEN_STUB := /home/withinboredom/code/php-src/build/gen_stub.php -LOCAL_FRANKENPHP := /home/withinboredom/code/frankenphp/caddy +LOCAL_FRANKENPHP := /home/withinboredom/code/frankenphp # Targets frankenphp: ext/build/ext.go ext/build/ext_arginfo.h @@ -22,7 +22,7 @@ frankenphp: ext/build/ext.go ext/build/ext_arginfo.h CGO_LDFLAGS="$(PHP_LDFLAGS) $(PHP_LIBS)" \ xcaddy build \ --output frankenphp \ - --with github.com/dunglas/frankenphp/caddy=$(LOCAL_FRANKENPHP) \ + --with github.com/dunglas/frankenphp=$(LOCAL_FRANKENPHP) \ --with github.com/bottledcode/durable-php/cli=$(LOCAL_MODULE) ext/build/ext_arginfo.h: ext/build/ext.stub.php From cd054a79cd3ebd9d8df280fc1bd4a5477df6a0b7 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sat, 9 Aug 2025 16:30:09 +0200 Subject: [PATCH 09/19] Revert "fixed build issues" This reverts commit 3a0475c9b86300521ac646fa6cf1e9b2c6e1a1ba. --- cli/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/Makefile b/cli/Makefile index a13d10fb..c73704a0 100644 --- a/cli/Makefile +++ b/cli/Makefile @@ -12,7 +12,7 @@ endif LOCAL_MODULE := /home/withinboredom/code/durable-php/cli GEN_STUB := /home/withinboredom/code/php-src/build/gen_stub.php -LOCAL_FRANKENPHP := /home/withinboredom/code/frankenphp +LOCAL_FRANKENPHP := /home/withinboredom/code/frankenphp/caddy # Targets frankenphp: ext/build/ext.go ext/build/ext_arginfo.h @@ -22,7 +22,7 @@ frankenphp: ext/build/ext.go ext/build/ext_arginfo.h CGO_LDFLAGS="$(PHP_LDFLAGS) $(PHP_LIBS)" \ xcaddy build \ --output frankenphp \ - --with github.com/dunglas/frankenphp=$(LOCAL_FRANKENPHP) \ + --with github.com/dunglas/frankenphp/caddy=$(LOCAL_FRANKENPHP) \ --with github.com/bottledcode/durable-php/cli=$(LOCAL_MODULE) ext/build/ext_arginfo.h: ext/build/ext.stub.php From b22b15ac663138f94f82e5681bf24ea71b269206 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sat, 9 Aug 2025 21:11:37 +0200 Subject: [PATCH 10/19] start working on getting the client up and running Signed-off-by: Robert Landers --- .idea/codeStyles/Project.xml | 1 - cli/Makefile | 6 +- cli/ext/build/ext.c | 38 ++++++++++++ cli/ext/build/ext.go | 58 ++++++++++++++++-- cli/ext/build/ext.h | 4 +- cli/ext/build/ext.stub.php | 2 + cli/ext/build/ext_arginfo.h | 7 ++- cli/lib/consumer.go | 1 + composer.json | 3 +- src/DurableClient.php | 40 ++++++++++++- src/Glue/autoload.php | 7 +-- src/Glue/glue.php | 113 +++++++++++++++++++++++------------ src/Glue/worker.php | 44 ++++++++++++++ 13 files changed, 269 insertions(+), 55 deletions(-) create mode 100644 src/Glue/worker.php diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index a17e6265..4c34f7c2 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -22,7 +22,6 @@