feat: rewrite bus to prevent deadlocks
All checks were successful
arcad/edge/pipeline/head This commit looks good
arcad/edge/pipeline/pr-master This commit looks good

This commit is contained in:
2023-11-28 16:35:49 +01:00
parent f4a7366aad
commit ad49c1718c
50 changed files with 1621 additions and 1336 deletions

View File

@ -39,21 +39,17 @@ func TestAppModuleWithMemoryRepository(t *testing.T) {
)),
)
file := "testdata/app.js"
script := "testdata/app.js"
data, err := os.ReadFile(file)
data, err := os.ReadFile(script)
if err != nil {
t.Fatal(err)
}
if err := server.Load(file, string(data)); err != nil {
t.Fatal(err)
ctx := context.Background()
if err := server.Start(ctx, script, string(data)); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
defer server.Stop()
ctx := context.Background()
if err := server.Start(ctx); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
}

View File

@ -2,8 +2,8 @@ package auth
import (
"context"
"io/ioutil"
"net/http"
"os"
"testing"
"time"
@ -22,7 +22,9 @@ import (
func TestAuthModule(t *testing.T) {
t.Parallel()
logger.SetLevel(slog.LevelDebug)
if testing.Verbose() {
logger.SetLevel(slog.LevelDebug)
}
key := getDummyKey()
@ -33,17 +35,15 @@ func TestAuthModule(t *testing.T) {
),
)
data, err := ioutil.ReadFile("testdata/auth.js")
script := "testdata/auth.js"
data, err := os.ReadFile(script)
if err != nil {
t.Fatal(err)
}
if err := server.Load("testdata/auth.js", string(data)); err != nil {
t.Fatal(err)
}
ctx := context.Background()
if err := server.Start(ctx); err != nil {
if err := server.Start(ctx, script, string(data)); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
@ -81,7 +81,9 @@ func TestAuthModule(t *testing.T) {
func TestAuthAnonymousModule(t *testing.T) {
t.Parallel()
logger.SetLevel(slog.LevelDebug)
if testing.Verbose() {
logger.SetLevel(slog.LevelDebug)
}
key := getDummyKey()
@ -90,17 +92,15 @@ func TestAuthAnonymousModule(t *testing.T) {
ModuleFactory(WithJWT(getDummyKeySet(key))),
)
data, err := ioutil.ReadFile("testdata/auth_anonymous.js")
script := "testdata/auth_anonymous.js"
data, err := os.ReadFile("testdata/auth_anonymous.js")
if err != nil {
t.Fatal(err)
}
if err := server.Load("testdata/auth_anonymous.js", string(data)); err != nil {
t.Fatal(err)
}
ctx := context.Background()
if err := server.Start(ctx); err != nil {
if err := server.Start(ctx, script, string(data)); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}

View File

@ -1,92 +0,0 @@
package blob
import (
"context"
"io"
"mime/multipart"
"forge.cadoles.com/arcad/edge/pkg/bus"
"forge.cadoles.com/arcad/edge/pkg/storage"
"github.com/oklog/ulid/v2"
)
const (
MessageNamespaceUploadRequest bus.MessageNamespace = "uploadRequest"
MessageNamespaceUploadResponse bus.MessageNamespace = "uploadResponse"
MessageNamespaceDownloadRequest bus.MessageNamespace = "downloadRequest"
MessageNamespaceDownloadResponse bus.MessageNamespace = "downloadResponse"
)
type MessageUploadRequest struct {
Context context.Context
RequestID string
FileHeader *multipart.FileHeader
Metadata map[string]interface{}
}
func (m *MessageUploadRequest) MessageNamespace() bus.MessageNamespace {
return MessageNamespaceUploadRequest
}
func NewMessageUploadRequest(ctx context.Context, fileHeader *multipart.FileHeader, metadata map[string]interface{}) *MessageUploadRequest {
return &MessageUploadRequest{
Context: ctx,
RequestID: ulid.Make().String(),
FileHeader: fileHeader,
Metadata: metadata,
}
}
type MessageUploadResponse struct {
RequestID string
BlobID storage.BlobID
Bucket string
Allow bool
}
func (m *MessageUploadResponse) MessageNamespace() bus.MessageNamespace {
return MessageNamespaceDownloadResponse
}
func NewMessageUploadResponse(requestID string) *MessageUploadResponse {
return &MessageUploadResponse{
RequestID: requestID,
}
}
type MessageDownloadRequest struct {
Context context.Context
RequestID string
Bucket string
BlobID storage.BlobID
}
func (m *MessageDownloadRequest) MessageNamespace() bus.MessageNamespace {
return MessageNamespaceDownloadRequest
}
func NewMessageDownloadRequest(ctx context.Context, bucket string, blobID storage.BlobID) *MessageDownloadRequest {
return &MessageDownloadRequest{
Context: ctx,
RequestID: ulid.Make().String(),
Bucket: bucket,
BlobID: blobID,
}
}
type MessageDownloadResponse struct {
RequestID string
Allow bool
BlobInfo storage.BlobInfo
Blob io.ReadSeekCloser
}
func (m *MessageDownloadResponse) MessageNamespace() bus.MessageNamespace {
return MessageNamespaceDownloadResponse
}
func NewMessageDownloadResponse(requestID string) *MessageDownloadResponse {
return &MessageDownloadResponse{
RequestID: requestID,
}
}

View File

@ -0,0 +1,55 @@
package blob
import (
"context"
"io"
"mime/multipart"
"forge.cadoles.com/arcad/edge/pkg/bus"
"forge.cadoles.com/arcad/edge/pkg/storage"
)
const (
AddressUpload bus.Address = "module/blob/upload"
AddressDownload bus.Address = "module/blob/download"
)
type UploadRequest struct {
Context context.Context
FileHeader *multipart.FileHeader
Metadata map[string]interface{}
}
func NewUploadRequestEnvelope(ctx context.Context, fileHeader *multipart.FileHeader, metadata map[string]interface{}) bus.Envelope {
return bus.NewEnvelope(AddressUpload, &UploadRequest{
Context: ctx,
FileHeader: fileHeader,
Metadata: metadata,
})
}
type UploadResponse struct {
Allow bool
Bucket string
BlobID storage.BlobID
}
type DownloadRequest struct {
Context context.Context
Bucket string
BlobID storage.BlobID
}
func NewDownloadRequestEnvelope(ctx context.Context, bucket string, blobID storage.BlobID) bus.Envelope {
return bus.NewEnvelope(AddressDownload, &DownloadRequest{
Context: ctx,
Bucket: bucket,
BlobID: blobID,
})
}
type DownloadResponse struct {
Allow bool
Blob io.ReadSeekCloser
BlobInfo storage.BlobInfo
}

212
pkg/module/blob/http.go Normal file
View File

@ -0,0 +1,212 @@
package blob
import (
"encoding/json"
"io/fs"
"mime/multipart"
"net/http"
"os"
"time"
"forge.cadoles.com/arcad/edge/pkg/bus"
edgehttp "forge.cadoles.com/arcad/edge/pkg/http"
"forge.cadoles.com/arcad/edge/pkg/storage"
"github.com/go-chi/chi/v5"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
type uploadResponse struct {
Bucket string `json:"bucket"`
BlobID storage.BlobID `json:"blobId"`
}
func Mount(uploadMaxFileSize int) func(r chi.Router) {
return func(r chi.Router) {
r.Post("/api/v1/upload", getAppUploadHandler(uploadMaxFileSize))
r.Get("/api/v1/download/{bucket}/{blobID}", handleAppDownload)
}
}
func getAppUploadHandler(fileMaxUpload int) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
uploadMaxFileSize := int64(8000)
r.Body = http.MaxBytesReader(w, r.Body, uploadMaxFileSize)
if err := r.ParseMultipartForm(uploadMaxFileSize); err != nil {
logger.Error(ctx, "could not parse multipart form", logger.CapturedE(errors.WithStack(err)))
edgehttp.JSONError(w, http.StatusBadRequest, edgehttp.ErrCodeBadRequest)
return
}
_, fileHeader, err := r.FormFile("file")
if err != nil {
logger.Error(ctx, "could not read form file", logger.CapturedE(errors.WithStack(err)))
edgehttp.JSONError(w, http.StatusBadRequest, edgehttp.ErrCodeBadRequest)
return
}
var metadata map[string]any
rawMetadata := r.Form.Get("metadata")
if rawMetadata != "" {
if err := json.Unmarshal([]byte(rawMetadata), &metadata); err != nil {
logger.Error(ctx, "could not parse metadata", logger.CapturedE(errors.WithStack(err)))
edgehttp.JSONError(w, http.StatusBadRequest, edgehttp.ErrCodeBadRequest)
return
}
}
requestEnv := NewUploadRequestEnvelope(ctx, fileHeader, metadata)
bus := edgehttp.ContextBus(ctx)
reply, err := bus.Request(ctx, requestEnv)
if err != nil {
logger.Error(ctx, "could not retrieve file", logger.CapturedE(errors.WithStack(err)))
edgehttp.JSONError(w, http.StatusInternalServerError, edgehttp.ErrCodeInternalError)
return
}
logger.Debug(ctx, "upload reply", logger.F("reply", reply))
replyMessage, ok := reply.Message().(*UploadResponse)
if !ok {
logger.Error(
ctx, "unexpected upload response message",
logger.F("message", reply.Message()),
)
edgehttp.JSONError(w, http.StatusInternalServerError, edgehttp.ErrCodeInternalError)
return
}
if !replyMessage.Allow {
edgehttp.JSONError(w, http.StatusForbidden, edgehttp.ErrCodeForbidden)
return
}
encoder := json.NewEncoder(w)
res := &uploadResponse{
Bucket: replyMessage.Bucket,
BlobID: replyMessage.BlobID,
}
if err := encoder.Encode(res); err != nil {
panic(errors.Wrap(err, "could not encode upload response"))
}
}
}
func handleAppDownload(w http.ResponseWriter, r *http.Request) {
bucket := chi.URLParam(r, "bucket")
blobID := chi.URLParam(r, "blobID")
ctx := logger.With(r.Context(), logger.F("blobID", blobID), logger.F("bucket", bucket))
requestMsg := NewDownloadRequestEnvelope(ctx, bucket, storage.BlobID(blobID))
bs := edgehttp.ContextBus(ctx)
reply, err := bs.Request(ctx, requestMsg)
if err != nil {
logger.Error(ctx, "could not retrieve file", logger.CapturedE(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
replyMessage, ok := reply.Message().(*DownloadResponse)
if !ok {
logger.Error(
ctx, "unexpected download response message",
logger.CapturedE(errors.WithStack(bus.ErrUnexpectedMessage)),
logger.F("message", reply),
)
edgehttp.JSONError(w, http.StatusInternalServerError, edgehttp.ErrCodeInternalError)
return
}
if !replyMessage.Allow {
edgehttp.JSONError(w, http.StatusForbidden, edgehttp.ErrCodeForbidden)
return
}
if replyMessage.Blob == nil {
edgehttp.JSONError(w, http.StatusNotFound, edgehttp.ErrCodeNotFound)
return
}
defer func() {
if err := replyMessage.Blob.Close(); err != nil {
logger.Error(ctx, "could not close blob", logger.CapturedE(errors.WithStack(err)))
}
}()
http.ServeContent(w, r, string(replyMessage.BlobInfo.ID()), replyMessage.BlobInfo.ModTime(), replyMessage.Blob)
}
type uploadedFile struct {
multipart.File
header *multipart.FileHeader
modTime time.Time
}
// Stat implements fs.File
func (f *uploadedFile) Stat() (fs.FileInfo, error) {
return &uploadedFileInfo{
header: f.header,
modTime: f.modTime,
}, nil
}
type uploadedFileInfo struct {
header *multipart.FileHeader
modTime time.Time
}
// IsDir implements fs.FileInfo
func (i *uploadedFileInfo) IsDir() bool {
return false
}
// ModTime implements fs.FileInfo
func (i *uploadedFileInfo) ModTime() time.Time {
return i.modTime
}
// Mode implements fs.FileInfo
func (i *uploadedFileInfo) Mode() fs.FileMode {
return os.ModePerm
}
// Name implements fs.FileInfo
func (i *uploadedFileInfo) Name() string {
return i.header.Filename
}
// Size implements fs.FileInfo
func (i *uploadedFileInfo) Size() int64 {
return i.header.Size
}
// Sys implements fs.FileInfo
func (i *uploadedFileInfo) Sys() any {
return nil
}
var (
_ fs.File = &uploadedFile{}
_ fs.FileInfo = &uploadedFileInfo{}
)

View File

@ -236,33 +236,34 @@ func (m *Module) getBucketSize(call goja.FunctionCall, rt *goja.Runtime) goja.Va
func (m *Module) handleMessages() {
ctx := context.Background()
go func() {
err := m.bus.Reply(ctx, MessageNamespaceUploadRequest, func(msg bus.Message) (bus.Message, error) {
uploadRequest, ok := msg.(*MessageUploadRequest)
if !ok {
return nil, errors.Wrapf(bus.ErrUnexpectedMessage, "expected message upload request, got '%T'", msg)
}
uploadRequestErrs := m.bus.Reply(ctx, AddressUpload, func(env bus.Envelope) (any, error) {
uploadRequest, ok := env.Message().(*UploadRequest)
if !ok {
return nil, errors.Wrapf(bus.ErrUnexpectedMessage, "expected message upload request, got '%T'", env.Message())
}
res, err := m.handleUploadRequest(uploadRequest)
if err != nil {
logger.Error(ctx, "could not handle upload request", logger.CapturedE(errors.WithStack(err)))
return nil, errors.WithStack(err)
}
logger.Debug(ctx, "upload request response", logger.F("response", res))
return res, nil
})
res, err := m.handleUploadRequest(uploadRequest)
if err != nil {
panic(errors.WithStack(err))
logger.Error(ctx, "could not handle upload request", logger.CapturedE(errors.WithStack(err)))
return nil, errors.WithStack(err)
}
logger.Debug(ctx, "upload request response", logger.F("response", res))
return res, nil
})
go func() {
for err := range uploadRequestErrs {
logger.Error(ctx, "error while replying to upload requests", logger.CapturedE(errors.WithStack(err)))
}
}()
err := m.bus.Reply(ctx, MessageNamespaceDownloadRequest, func(msg bus.Message) (bus.Message, error) {
downloadRequest, ok := msg.(*MessageDownloadRequest)
downloadRequestErrs := m.bus.Reply(ctx, AddressDownload, func(env bus.Envelope) (any, error) {
downloadRequest, ok := env.Message().(*DownloadRequest)
if !ok {
return nil, errors.Wrapf(bus.ErrUnexpectedMessage, "expected message download request, got '%T'", msg)
return nil, errors.Wrapf(bus.ErrUnexpectedMessage, "expected message download request, got '%T'", env.Message())
}
res, err := m.handleDownloadRequest(downloadRequest)
@ -274,14 +275,15 @@ func (m *Module) handleMessages() {
return res, nil
})
if err != nil {
panic(errors.WithStack(err))
for err := range downloadRequestErrs {
logger.Fatal(ctx, "error while replying to download requests", logger.CapturedE(errors.WithStack(err)))
}
}
func (m *Module) handleUploadRequest(req *MessageUploadRequest) (*MessageUploadResponse, error) {
func (m *Module) handleUploadRequest(req *UploadRequest) (*UploadResponse, error) {
blobID := storage.NewBlobID()
res := NewMessageUploadResponse(req.RequestID)
res := &UploadResponse{}
ctx := logger.With(req.Context, logger.F("blobID", blobID))
@ -302,11 +304,11 @@ func (m *Module) handleUploadRequest(req *MessageUploadRequest) (*MessageUploadR
return nil, errors.WithStack(err)
}
result, ok := rawResult.Export().(map[string]interface{})
result, ok := rawResult.(map[string]interface{})
if !ok {
return nil, errors.Errorf(
"unexpected onBlobUpload result: expected 'map[string]interface{}', got '%T'",
rawResult.Export(),
rawResult,
)
}
@ -393,8 +395,8 @@ func (m *Module) saveBlob(ctx context.Context, bucketName string, blobID storage
return nil
}
func (m *Module) handleDownloadRequest(req *MessageDownloadRequest) (*MessageDownloadResponse, error) {
res := NewMessageDownloadResponse(req.RequestID)
func (m *Module) handleDownloadRequest(req *DownloadRequest) (*DownloadResponse, error) {
res := &DownloadResponse{}
rawResult, err := m.server.ExecFuncByName(req.Context, "onBlobDownload", req.Context, req.Bucket, req.BlobID)
if err != nil {
@ -407,11 +409,11 @@ func (m *Module) handleDownloadRequest(req *MessageDownloadRequest) (*MessageDow
return nil, errors.WithStack(err)
}
result, ok := rawResult.Export().(map[string]interface{})
result, ok := rawResult.(map[string]interface{})
if !ok {
return nil, errors.Errorf(
"unexpected onBlobDownload result: expected 'map[string]interface{}', got '%T'",
rawResult.Export(),
rawResult,
)
}

View File

@ -17,7 +17,9 @@ import (
func TestBlobModule(t *testing.T) {
t.Parallel()
logger.SetLevel(slog.LevelDebug)
if testing.Verbose() {
logger.SetLevel(slog.LevelDebug)
}
bus := memory.NewBus()
store := sqlite.NewBlobStore(":memory:?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000")
@ -28,19 +30,17 @@ func TestBlobModule(t *testing.T) {
ModuleFactory(bus, store),
)
data, err := os.ReadFile("testdata/blob.js")
script := "testdata/blob.js"
data, err := os.ReadFile(script)
if err != nil {
t.Fatal(err)
}
if err := server.Load("testdata/blob.js", string(data)); err != nil {
t.Fatal(err)
ctx := context.Background()
if err := server.Start(ctx, script, string(data)); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
defer server.Stop()
ctx := context.Background()
if err := server.Start(ctx); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
}

View File

@ -21,7 +21,9 @@ func TestCastLoadURL(t *testing.T) {
return
}
logger.SetLevel(slog.LevelDebug)
if testing.Verbose() {
logger.SetLevel(slog.LevelDebug)
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

View File

@ -2,7 +2,6 @@ package cast
import (
"context"
"io/ioutil"
"os"
"testing"
"time"
@ -24,24 +23,24 @@ func TestCastModule(t *testing.T) {
return
}
logger.SetLevel(slog.LevelDebug)
if testing.Verbose() {
logger.SetLevel(slog.LevelDebug)
}
server := app.NewServer(
module.ConsoleModuleFactory(),
CastModuleFactory(),
)
data, err := ioutil.ReadFile("testdata/cast.js")
script := "testdata/cast.js"
data, err := os.ReadFile(script)
if err != nil {
t.Fatal(err)
}
if err := server.Load("testdata/cast.js", string(data)); err != nil {
t.Fatal(err)
}
ctx := context.Background()
if err := server.Start(ctx); err != nil {
if err := server.Start(ctx, script, string(data)); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
@ -59,24 +58,24 @@ func TestCastModuleRefreshDevices(t *testing.T) {
return
}
logger.SetLevel(slog.LevelDebug)
if testing.Verbose() {
logger.SetLevel(slog.LevelDebug)
}
server := app.NewServer(
module.ConsoleModuleFactory(),
CastModuleFactory(),
)
data, err := ioutil.ReadFile("testdata/refresh_devices.js")
script := "testdata/refresh_devices.js"
data, err := os.ReadFile(script)
if err != nil {
t.Fatal(err)
}
if err := server.Load("testdata/refresh_devices.js", string(data)); err != nil {
t.Fatal(err)
}
ctx := context.Background()
if err := server.Start(ctx); err != nil {
if err := server.Start(ctx, script, string(data)); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
@ -87,12 +86,5 @@ func TestCastModuleRefreshDevices(t *testing.T) {
t.Error(errors.WithStack(err))
}
promise, ok := app.IsPromise(result)
if !ok {
t.Fatal("expected promise")
}
value := server.WaitForPromise(promise)
spew.Dump(value.Export())
spew.Dump(result)
}

View File

@ -0,0 +1,38 @@
package fetch
import (
"context"
"net/url"
"forge.cadoles.com/arcad/edge/pkg/bus"
)
const (
AddressFetchRequest bus.Address = "module/fetch/request"
AddressFetchResponse bus.Address = "module/fetch/response"
)
type FetchRequest struct {
Context context.Context
RequestID string
URL *url.URL
RemoteAddr string
}
func NewFetchRequestEnvelope(ctx context.Context, remoteAddr string, url *url.URL) bus.Envelope {
return bus.NewEnvelope(AddressFetchRequest, &FetchRequest{
Context: ctx,
URL: url,
RemoteAddr: remoteAddr,
})
}
type FetchResponse struct {
Allow bool
}
func NewFetchResponseEnvelope(allow bool) bus.Envelope {
return bus.NewEnvelope(AddressFetchResponse, &FetchResponse{
Allow: allow,
})
}

View File

@ -1,49 +0,0 @@
package fetch
import (
"context"
"net/url"
"forge.cadoles.com/arcad/edge/pkg/bus"
"github.com/oklog/ulid/v2"
)
const (
MessageNamespaceFetchRequest bus.MessageNamespace = "fetchRequest"
MessageNamespaceFetchResponse bus.MessageNamespace = "fetchResponse"
)
type MessageFetchRequest struct {
Context context.Context
RequestID string
URL *url.URL
RemoteAddr string
}
func (m *MessageFetchRequest) MessageNamespace() bus.MessageNamespace {
return MessageNamespaceFetchRequest
}
func NewMessageFetchRequest(ctx context.Context, remoteAddr string, url *url.URL) *MessageFetchRequest {
return &MessageFetchRequest{
Context: ctx,
RequestID: ulid.Make().String(),
RemoteAddr: remoteAddr,
URL: url,
}
}
type MessageFetchResponse struct {
RequestID string
Allow bool
}
func (m *MessageFetchResponse) MessageNamespace() bus.MessageNamespace {
return MessageNamespaceFetchResponse
}
func NewMessageFetchResponse(requestID string) *MessageFetchResponse {
return &MessageFetchResponse{
RequestID: requestID,
}
}

115
pkg/module/fetch/http.go Normal file
View File

@ -0,0 +1,115 @@
package fetch
import (
"io"
"net/http"
"net/url"
edgehttp "forge.cadoles.com/arcad/edge/pkg/http"
"github.com/go-chi/chi/v5"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
func Mount() func(r chi.Router) {
return func(r chi.Router) {
r.Get("/api/v1/fetch", handleAppFetch)
}
}
func handleAppFetch(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
rawURL := r.URL.Query().Get("url")
url, err := url.Parse(rawURL)
if err != nil {
edgehttp.JSONError(w, http.StatusBadRequest, edgehttp.ErrCodeBadRequest)
return
}
requestMsg := NewFetchRequestEnvelope(ctx, r.RemoteAddr, url)
bus := edgehttp.ContextBus(ctx)
reply, err := bus.Request(ctx, requestMsg)
if err != nil {
logger.Error(ctx, "could not retrieve fetch request reply", logger.CapturedE(errors.WithStack(err)))
edgehttp.JSONError(w, http.StatusInternalServerError, edgehttp.ErrCodeInternalError)
return
}
logger.Debug(ctx, "fetch reply", logger.F("reply", reply))
responseMsg, ok := reply.Message().(*FetchResponse)
if !ok {
logger.Error(
ctx, "unexpected fetch response message",
logger.F("message", reply),
)
edgehttp.JSONError(w, http.StatusInternalServerError, edgehttp.ErrCodeInternalError)
return
}
if !responseMsg.Allow {
edgehttp.JSONError(w, http.StatusForbidden, edgehttp.ErrCodeForbidden)
return
}
proxyReq, err := http.NewRequest(http.MethodGet, url.String(), nil)
if err != nil {
logger.Error(
ctx, "could not create proxy request",
logger.CapturedE(errors.WithStack(err)),
)
edgehttp.JSONError(w, http.StatusInternalServerError, edgehttp.ErrCodeInternalError)
return
}
for header, values := range r.Header {
for _, value := range values {
proxyReq.Header.Add(header, value)
}
}
proxyReq.Header.Add("X-Forwarded-From", r.RemoteAddr)
httpClient := edgehttp.ContextHTTPClient(ctx)
res, err := httpClient.Do(proxyReq)
if err != nil {
logger.Error(
ctx, "could not execute proxy request",
logger.CapturedE(errors.WithStack(err)),
)
edgehttp.JSONError(w, http.StatusInternalServerError, edgehttp.ErrCodeInternalError)
return
}
defer func() {
if err := res.Body.Close(); err != nil {
logger.Error(
ctx, "could not close response body",
logger.CapturedE(errors.WithStack(err)),
)
}
}()
for header, values := range res.Header {
for _, value := range values {
w.Header().Add(header, value)
}
}
w.WriteHeader(res.StatusCode)
if _, err := io.Copy(w, res.Body); err != nil {
panic(errors.WithStack(err))
}
}

View File

@ -40,10 +40,10 @@ func (m *Module) get(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
func (m *Module) handleMessages() {
ctx := context.Background()
err := m.bus.Reply(ctx, MessageNamespaceFetchRequest, func(msg bus.Message) (bus.Message, error) {
fetchRequest, ok := msg.(*MessageFetchRequest)
fetchErrs := m.bus.Reply(ctx, AddressFetchRequest, func(env bus.Envelope) (any, error) {
fetchRequest, ok := env.Message().(*FetchRequest)
if !ok {
return nil, errors.Wrapf(bus.ErrUnexpectedMessage, "expected message fetch request, got '%T'", msg)
return nil, errors.Wrapf(bus.ErrUnexpectedMessage, "expected fetch request, got '%T'", env.Message())
}
res, err := m.handleFetchRequest(fetchRequest)
@ -57,13 +57,14 @@ func (m *Module) handleMessages() {
return res, nil
})
if err != nil {
panic(errors.WithStack(err))
for err := range fetchErrs {
logger.Fatal(ctx, "error while replying to fetch requests", logger.CapturedE(errors.WithStack(err)))
}
}
func (m *Module) handleFetchRequest(req *MessageFetchRequest) (*MessageFetchResponse, error) {
res := NewMessageFetchResponse(req.RequestID)
func (m *Module) handleFetchRequest(req *FetchRequest) (*FetchResponse, error) {
res := &FetchResponse{}
ctx := logger.With(
req.Context,
@ -83,11 +84,11 @@ func (m *Module) handleFetchRequest(req *MessageFetchRequest) (*MessageFetchResp
return nil, errors.WithStack(err)
}
result, ok := rawResult.Export().(map[string]interface{})
result, ok := rawResult.(map[string]interface{})
if !ok {
return nil, errors.Errorf(
"unexpected onClientFetch result: expected 'map[string]interface{}', got '%T'",
rawResult.Export(),
rawResult,
)
}

View File

@ -2,8 +2,8 @@ package fetch
import (
"context"
"io/ioutil"
"net/url"
"os"
"testing"
"time"
@ -18,7 +18,9 @@ import (
func TestFetchModule(t *testing.T) {
t.Parallel()
logger.SetLevel(slog.LevelDebug)
if testing.Verbose() {
logger.SetLevel(slog.LevelDebug)
}
bus := memory.NewBus()
@ -28,22 +30,20 @@ func TestFetchModule(t *testing.T) {
ModuleFactory(bus),
)
data, err := ioutil.ReadFile("testdata/fetch.js")
path := "testdata/fetch.js"
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if err := server.Load("testdata/fetch.js", string(data)); err != nil {
ctx := context.Background()
if err := server.Start(ctx, path, string(data)); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
defer server.Stop()
ctx := context.Background()
if err := server.Start(ctx); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
// Wait for module to startup
time.Sleep(1 * time.Second)
@ -53,33 +53,33 @@ func TestFetchModule(t *testing.T) {
remoteAddr := "127.0.0.1"
url, _ := url.Parse("http://example.com")
rawReply, err := bus.Request(ctx, NewMessageFetchRequest(ctx, remoteAddr, url))
reply, err := bus.Request(ctx, NewFetchRequestEnvelope(ctx, remoteAddr, url))
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
reply, ok := rawReply.(*MessageFetchResponse)
response, ok := reply.Message().(*FetchResponse)
if !ok {
t.Fatalf("unexpected reply type '%T'", rawReply)
t.Fatalf("unexpected reply message type '%T'", reply.Message())
}
if e, g := true, reply.Allow; e != g {
if e, g := true, response.Allow; e != g {
t.Errorf("reply.Allow: expected '%v', got '%v'", e, g)
}
url, _ = url.Parse("https://google.com")
rawReply, err = bus.Request(ctx, NewMessageFetchRequest(ctx, remoteAddr, url))
reply, err = bus.Request(ctx, NewFetchRequestEnvelope(ctx, remoteAddr, url))
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
reply, ok = rawReply.(*MessageFetchResponse)
response, ok = reply.Message().(*FetchResponse)
if !ok {
t.Fatalf("unexpected reply type '%T'", rawReply)
t.Fatalf("unexpected reply message type '%T'", reply.Message())
}
if e, g := false, reply.Allow; e != g {
if e, g := false, response.Allow; e != g {
t.Errorf("reply.Allow: expected '%v', got '%v'", e, g)
}
}

View File

@ -5,7 +5,6 @@ import (
"forge.cadoles.com/arcad/edge/pkg/app"
"github.com/dop251/goja"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
@ -19,16 +18,28 @@ func (m *LifecycleModule) Export(export *goja.Object) {
}
func (m *LifecycleModule) OnInit(ctx context.Context, rt *goja.Runtime) (err error) {
_, ok := goja.AssertFunction(rt.Get("onInit"))
call, ok := goja.AssertFunction(rt.Get("onInit"))
if !ok {
logger.Warn(ctx, "could not find onInit() function")
return nil
}
if _, err := rt.RunString("setTimeout(onInit, 0)"); err != nil {
return errors.WithStack(err)
}
defer func() {
recovered := recover()
if recovered == nil {
return
}
recoveredErr, ok := recovered.(error)
if !ok {
panic(recovered)
}
err = recoveredErr
}()
call(nil, rt.ToValue(ctx))
return nil
}

View File

@ -1,38 +0,0 @@
package module
import (
"context"
"forge.cadoles.com/arcad/edge/pkg/bus"
)
const (
MessageNamespaceClient bus.MessageNamespace = "client"
MessageNamespaceServer bus.MessageNamespace = "server"
)
type ServerMessage struct {
Context context.Context
Data interface{}
}
func (m *ServerMessage) MessageNamespace() bus.MessageNamespace {
return MessageNamespaceServer
}
func NewServerMessage(ctx context.Context, data interface{}) *ServerMessage {
return &ServerMessage{ctx, data}
}
type ClientMessage struct {
Context context.Context
Data map[string]interface{}
}
func (m *ClientMessage) MessageNamespace() bus.MessageNamespace {
return MessageNamespaceClient
}
func NewClientMessage(ctx context.Context, data map[string]interface{}) *ClientMessage {
return &ClientMessage{ctx, data}
}

View File

@ -5,7 +5,7 @@ import (
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/bus"
edgeHTTP "forge.cadoles.com/arcad/edge/pkg/http"
edgehttp "forge.cadoles.com/arcad/edge/pkg/http"
"forge.cadoles.com/arcad/edge/pkg/module"
"forge.cadoles.com/arcad/edge/pkg/module/util"
"github.com/dop251/goja"
@ -38,10 +38,9 @@ func (m *Module) broadcast(call goja.FunctionCall, rt *goja.Runtime) goja.Value
}
data := call.Argument(0).Export()
ctx := context.Background()
msg := module.NewServerMessage(ctx, data)
if err := m.bus.Publish(ctx, msg); err != nil {
env := edgehttp.NewOutgoingMessageEnvelope("", data)
if err := m.bus.Publish(env); err != nil {
panic(rt.ToValue(errors.WithStack(err)))
}
@ -53,38 +52,33 @@ func (m *Module) send(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
panic(rt.ToValue(errors.New("invalid number of argument")))
}
var ctx context.Context
firstArg := call.Argument(0)
sessionID, ok := firstArg.Export().(string)
if ok {
ctx = module.WithContext(context.Background(), map[module.ContextKey]any{
edgeHTTP.ContextKeySessionID: sessionID,
})
} else {
ctx = util.AssertContext(firstArg, rt)
if !ok {
ctx := util.AssertContext(firstArg, rt)
sessionID = module.ContextValue[string](ctx, edgehttp.ContextKeySessionID)
}
data := call.Argument(1).Export()
msg := module.NewServerMessage(ctx, data)
if err := m.bus.Publish(ctx, msg); err != nil {
env := edgehttp.NewOutgoingMessageEnvelope(sessionID, data)
if err := m.bus.Publish(env); err != nil {
panic(rt.ToValue(errors.WithStack(err)))
}
return nil
}
func (m *Module) handleClientMessages() {
func (m *Module) handleIncomingMessages() {
ctx := context.Background()
logger.Debug(
ctx,
"subscribing to bus messages",
"subscribing to bus envelopes",
)
clientMessages, err := m.bus.Subscribe(ctx, module.MessageNamespaceClient)
envelopes, err := m.bus.Subscribe(ctx, edgehttp.AddressIncomingMessage)
if err != nil {
panic(errors.WithStack(err))
}
@ -92,16 +86,16 @@ func (m *Module) handleClientMessages() {
defer func() {
logger.Debug(
ctx,
"unsubscribing from bus messages",
"unsubscribing from bus envelopes",
)
m.bus.Unsubscribe(ctx, module.MessageNamespaceClient, clientMessages)
m.bus.Unsubscribe(edgehttp.AddressIncomingMessage, envelopes)
}()
for {
logger.Debug(
ctx,
"waiting for next message",
"waiting for next envelope",
)
select {
case <-ctx.Done():
@ -112,13 +106,13 @@ func (m *Module) handleClientMessages() {
return
case msg := <-clientMessages:
clientMessage, ok := msg.(*module.ClientMessage)
case env := <-envelopes:
incomingMessage, ok := env.Message().(*edgehttp.IncomingMessage)
if !ok {
logger.Warn(
ctx,
"unexpected message type",
logger.F("message", msg),
logger.F("message", env.Message()),
)
continue
@ -126,11 +120,11 @@ func (m *Module) handleClientMessages() {
logger.Debug(
ctx,
"received client message",
logger.F("message", clientMessage),
"received incoming message",
logger.F("message", incomingMessage),
)
if _, err := m.server.ExecFuncByName(clientMessage.Context, "onClientMessage", clientMessage.Context, clientMessage.Data); err != nil {
if _, err := m.server.ExecFuncByName(incomingMessage.Context, "onClientMessage", incomingMessage.Context, incomingMessage.Payload); err != nil {
if errors.Is(err, app.ErrFuncDoesNotExist) {
continue
}
@ -152,7 +146,7 @@ func ModuleFactory(bus bus.Bus) app.ServerModuleFactory {
bus: bus,
}
go module.handleClientMessages()
go module.handleIncomingMessages()
return module
}

View File

@ -1,278 +0,0 @@
package module
import (
"context"
"fmt"
"sync"
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/bus"
"forge.cadoles.com/arcad/edge/pkg/module/util"
"github.com/dop251/goja"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
type RPCRequest struct {
Method string
Params interface{}
ID interface{}
}
type RPCError struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
type RPCResponse struct {
Result interface{}
Error *RPCError
ID interface{}
}
type RPCModule struct {
server *app.Server
bus bus.Bus
callbacks sync.Map
}
func (m *RPCModule) Name() string {
return "rpc"
}
func (m *RPCModule) Export(export *goja.Object) {
if err := export.Set("register", m.register); err != nil {
panic(errors.Wrap(err, "could not set 'register' function"))
}
if err := export.Set("unregister", m.unregister); err != nil {
panic(errors.Wrap(err, "could not set 'unregister' function"))
}
}
func (m *RPCModule) OnInit(ctx context.Context, rt *goja.Runtime) error {
go m.handleMessages(ctx)
return nil
}
func (m *RPCModule) register(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
fnName := util.AssertString(call.Argument(0), rt)
var (
callable goja.Callable
ok bool
)
if len(call.Arguments) > 1 {
callable, ok = goja.AssertFunction(call.Argument(1))
} else {
callable, ok = goja.AssertFunction(rt.Get(fnName))
}
if !ok {
panic(rt.NewTypeError("method should be a valid function"))
}
ctx := context.Background()
logger.Debug(ctx, "registering method", logger.F("method", fnName))
m.callbacks.Store(fnName, callable)
return nil
}
func (m *RPCModule) unregister(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
fnName := util.AssertString(call.Argument(0), rt)
m.callbacks.Delete(fnName)
return nil
}
func (m *RPCModule) handleMessages(ctx context.Context) {
clientMessages, err := m.bus.Subscribe(ctx, MessageNamespaceClient)
if err != nil {
panic(errors.WithStack(err))
}
defer func() {
m.bus.Unsubscribe(ctx, MessageNamespaceClient, clientMessages)
}()
sendRes := func(ctx context.Context, req *RPCRequest, result goja.Value) {
res := &RPCResponse{
ID: req.ID,
Result: result.Export(),
}
logger.Debug(ctx, "sending rpc response", logger.F("response", res))
if err := m.sendResponse(ctx, res); err != nil {
logger.Error(
ctx, "could not send response",
logger.CapturedE(errors.WithStack(err)),
logger.F("response", res),
logger.F("request", req),
)
}
}
for msg := range clientMessages {
go m.handleMessage(ctx, msg, sendRes)
}
}
func (m *RPCModule) handleMessage(ctx context.Context, msg bus.Message, sendRes func(ctx context.Context, req *RPCRequest, result goja.Value)) {
clientMessage, ok := msg.(*ClientMessage)
if !ok {
logger.Warn(ctx, "unexpected bus message", logger.F("message", msg))
return
}
ok, req := m.isRPCRequest(clientMessage)
if !ok {
return
}
logger.Debug(ctx, "received rpc request", logger.F("request", req))
rawCallable, exists := m.callbacks.Load(req.Method)
if !exists {
logger.Debug(ctx, "method not found", logger.F("req", req))
if err := m.sendMethodNotFoundResponse(clientMessage.Context, req); err != nil {
logger.Error(
ctx, "could not send method not found response",
logger.CapturedE(errors.WithStack(err)),
logger.F("request", req),
)
}
return
}
callable, ok := rawCallable.(goja.Callable)
if !ok {
logger.Debug(ctx, "invalid method", logger.F("req", req))
if err := m.sendMethodNotFoundResponse(clientMessage.Context, req); err != nil {
logger.Error(
ctx, "could not send method not found response",
logger.CapturedE(errors.WithStack(err)),
logger.F("request", req),
)
}
return
}
result, err := m.server.Exec(clientMessage.Context, callable, clientMessage.Context, req.Params)
if err != nil {
logger.Error(
ctx, "rpc call error",
logger.CapturedE(errors.WithStack(err)),
logger.F("request", req),
)
if err := m.sendErrorResponse(clientMessage.Context, req, err); err != nil {
logger.Error(
ctx, "could not send error response",
logger.CapturedE(errors.WithStack(err)),
logger.F("originalError", err),
logger.F("request", req),
)
}
return
}
promise, ok := app.IsPromise(result)
if ok {
go func(ctx context.Context, req *RPCRequest, promise *goja.Promise) {
result := m.server.WaitForPromise(promise)
sendRes(ctx, req, result)
}(clientMessage.Context, req, promise)
} else {
sendRes(clientMessage.Context, req, result)
}
}
func (m *RPCModule) sendErrorResponse(ctx context.Context, req *RPCRequest, err error) error {
return m.sendResponse(ctx, &RPCResponse{
ID: req.ID,
Result: nil,
Error: &RPCError{
Code: -32603,
Message: err.Error(),
},
})
}
func (m *RPCModule) sendMethodNotFoundResponse(ctx context.Context, req *RPCRequest) error {
return m.sendResponse(ctx, &RPCResponse{
ID: req.ID,
Result: nil,
Error: &RPCError{
Code: -32601,
Message: fmt.Sprintf("method not found"),
},
})
}
func (m *RPCModule) sendResponse(ctx context.Context, res *RPCResponse) error {
msg := NewServerMessage(ctx, map[string]interface{}{
"jsonrpc": "2.0",
"id": res.ID,
"error": res.Error,
"result": res.Result,
})
if err := m.bus.Publish(ctx, msg); err != nil {
return errors.WithStack(err)
}
return nil
}
func (m *RPCModule) isRPCRequest(msg *ClientMessage) (bool, *RPCRequest) {
jsonRPC, exists := msg.Data["jsonrpc"]
if !exists || jsonRPC != "2.0" {
return false, nil
}
rawMethod, exists := msg.Data["method"]
if !exists {
return false, nil
}
method, ok := rawMethod.(string)
if !ok {
return false, nil
}
id := msg.Data["id"]
params := msg.Data["params"]
return true, &RPCRequest{
ID: id,
Method: method,
Params: params,
}
}
func RPCModuleFactory(bus bus.Bus) app.ServerModuleFactory {
return func(server *app.Server) app.ServerModule {
mod := &RPCModule{
server: server,
bus: bus,
}
return mod
}
}
var _ app.InitializableModule = &RPCModule{}

View File

@ -0,0 +1,21 @@
package rpc
import (
"context"
"forge.cadoles.com/arcad/edge/pkg/bus"
)
const (
Address bus.Address = "module/rpc"
)
type Request struct {
Context context.Context
Method string
Params any
}
func NewRequestEnvelope(ctx context.Context, method string, params any) bus.Envelope {
return bus.NewEnvelope(Address, &Request{ctx, method, params})
}

7
pkg/module/rpc/error.go Normal file
View File

@ -0,0 +1,7 @@
package rpc
import "errors"
var (
ErrMethodNotFound = errors.New("method not found")
)

19
pkg/module/rpc/jsonrpc.go Normal file
View File

@ -0,0 +1,19 @@
package rpc
import "fmt"
type JSONRPCRequest struct {
ID any
Method string
Params any
}
type JSONRPCError struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
func (e *JSONRPCError) Error() string {
return fmt.Sprintf("json-rpc error: %d - %s", e.Code, e.Message)
}

256
pkg/module/rpc/module.go Normal file
View File

@ -0,0 +1,256 @@
package rpc
import (
"context"
"sync"
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/bus"
edgehttp "forge.cadoles.com/arcad/edge/pkg/http"
"forge.cadoles.com/arcad/edge/pkg/module"
"forge.cadoles.com/arcad/edge/pkg/module/util"
"github.com/dop251/goja"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
type Module struct {
server *app.Server
bus bus.Bus
callbacks sync.Map
}
func (m *Module) Name() string {
return "rpc"
}
func (m *Module) Export(export *goja.Object) {
if err := export.Set("register", m.register); err != nil {
panic(errors.Wrap(err, "could not set 'register' function"))
}
if err := export.Set("unregister", m.unregister); err != nil {
panic(errors.Wrap(err, "could not set 'unregister' function"))
}
}
func (m *Module) OnInit(ctx context.Context, rt *goja.Runtime) error {
requestErrs := m.bus.Reply(ctx, Address, m.handleRequest)
go func() {
for err := range requestErrs {
logger.Error(ctx, "error while replying to rpc requests", logger.CapturedE(errors.WithStack(err)))
}
}()
httpIncomingMessages, err := m.bus.Subscribe(ctx, edgehttp.AddressIncomingMessage)
if err != nil {
return errors.WithStack(err)
}
go m.handleIncomingHTTPMessages(ctx, httpIncomingMessages)
return nil
}
func (m *Module) register(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
fnName := util.AssertString(call.Argument(0), rt)
var (
callable goja.Callable
ok bool
)
if len(call.Arguments) > 1 {
callable, ok = goja.AssertFunction(call.Argument(1))
} else {
callable, ok = goja.AssertFunction(rt.Get(fnName))
}
if !ok {
panic(rt.NewTypeError("method should be a valid function"))
}
ctx := context.Background()
logger.Debug(ctx, "registering method", logger.F("method", fnName))
m.callbacks.Store(fnName, callable)
return nil
}
func (m *Module) unregister(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
fnName := util.AssertString(call.Argument(0), rt)
m.callbacks.Delete(fnName)
return nil
}
func (m *Module) handleRequest(env bus.Envelope) (any, error) {
request, ok := env.Message().(*Request)
if !ok {
logger.Warn(context.Background(), "unexpected bus message", logger.F("message", env.Message()))
return nil, errors.WithStack(bus.ErrUnexpectedMessage)
}
ctx := logger.With(request.Context, logger.F("request", request))
logger.Debug(ctx, "received rpc request")
rawCallable, exists := m.callbacks.Load(request.Method)
if !exists {
logger.Debug(ctx, "method not found")
return nil, errors.WithStack(ErrMethodNotFound)
}
callable, ok := rawCallable.(goja.Callable)
if !ok {
logger.Debug(ctx, "invalid method")
return nil, errors.WithStack(ErrMethodNotFound)
}
result, err := m.server.Exec(ctx, callable, request.Context, request.Params)
if err != nil {
logger.Error(
ctx, "rpc call error",
logger.CapturedE(errors.WithStack(err)),
)
return nil, errors.WithStack(err)
}
return result, nil
}
func (m *Module) handleIncomingHTTPMessages(ctx context.Context, incoming <-chan bus.Envelope) {
defer func() {
m.bus.Unsubscribe(edgehttp.AddressIncomingMessage, incoming)
}()
for env := range incoming {
msg, ok := env.Message().(*edgehttp.IncomingMessage)
if !ok {
logger.Error(ctx, "unexpected incoming http message type", logger.F("message", env.Message()))
continue
}
jsonReq, ok := m.isRPCRequest(msg.Payload)
if !ok {
continue
}
requestCtx := logger.With(msg.Context, logger.F("rpcRequestMethod", jsonReq.Method), logger.F("rpcRequestID", jsonReq.ID))
request := NewRequestEnvelope(msg.Context, jsonReq.Method, jsonReq.Params)
sessionID := module.ContextValue[string](msg.Context, edgehttp.ContextKeySessionID)
reply, err := m.bus.Request(requestCtx, request)
if err != nil {
err = errors.WithStack(err)
logger.Error(
ctx, "could not execute rpc request",
logger.CapturedE(err),
)
if errors.Is(err, ErrMethodNotFound) {
if err := m.sendMethodNotFoundResponse(sessionID, jsonReq.ID); err != nil {
logger.Error(
ctx, "could not send json rpc error response",
logger.CapturedE(errors.WithStack(err)),
)
}
continue
}
if err := m.sendErrorResponse(sessionID, jsonReq.ID, err); err != nil {
logger.Error(
ctx, "could not send json rpc error response",
logger.CapturedE(errors.WithStack(err)),
)
}
continue
}
if err := m.sendResponse(sessionID, jsonReq.ID, reply.Message(), nil); err != nil {
logger.Error(
ctx, "could not send json rpc result response",
logger.CapturedE(err),
)
}
}
}
func (m *Module) sendErrorResponse(sessionID string, requestID any, err error) error {
return m.sendResponse(sessionID, requestID, nil, &JSONRPCError{
Code: -32603,
Message: err.Error(),
})
}
func (m *Module) sendMethodNotFoundResponse(sessionID string, requestID any) error {
return m.sendResponse(sessionID, requestID, nil, &JSONRPCError{
Code: -32601,
Message: "method not found",
})
}
func (m *Module) sendResponse(sessionID string, requestID any, result any, err error) error {
env := edgehttp.NewOutgoingMessageEnvelope(sessionID, map[string]interface{}{
"jsonrpc": "2.0",
"id": requestID,
"error": err,
"result": result,
})
if err := m.bus.Publish(env); err != nil {
return errors.WithStack(err)
}
return nil
}
func (m *Module) isRPCRequest(payload map[string]any) (*JSONRPCRequest, bool) {
jsonRPC, exists := payload["jsonrpc"]
if !exists || jsonRPC != "2.0" {
return nil, false
}
rawMethod, exists := payload["method"]
if !exists {
return nil, false
}
method, ok := rawMethod.(string)
if !ok {
return nil, false
}
id := payload["id"]
params := payload["params"]
return &JSONRPCRequest{
ID: id,
Method: method,
Params: params,
}, true
}
func ModuleFactory(bus bus.Bus) app.ServerModuleFactory {
return func(server *app.Server) app.ServerModule {
mod := &Module{
server: server,
bus: bus,
}
return mod
}
}
var _ app.InitializableModule = &Module{}

View File

@ -0,0 +1,109 @@
package rpc
import (
"context"
"os"
"sync"
"testing"
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/bus"
"forge.cadoles.com/arcad/edge/pkg/bus/memory"
"forge.cadoles.com/arcad/edge/pkg/module"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
func TestServerExecDeadlock(t *testing.T) {
if testing.Verbose() {
logger.SetLevel(logger.LevelDebug)
}
b := memory.NewBus(memory.WithBufferSize(1))
server := app.NewServer(
module.ConsoleModuleFactory(),
ModuleFactory(b),
module.LifecycleModuleFactory(),
)
data, err := os.ReadFile("testdata/deadlock.js")
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
ctx := context.Background()
t.Log("starting server")
if err := server.Start(ctx, "deadlock.js", string(data)); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
defer server.Stop()
t.Log("server started")
count := 100
delay := 100
var wg sync.WaitGroup
wg.Add(count)
for i := 0; i < count; i++ {
go func(i int) {
defer wg.Done()
t.Logf("calling %d", i)
isCanceled := i%2 == 0
var ctx context.Context
if isCanceled {
canceledCtx, cancel := context.WithCancel(context.Background())
cancel()
ctx = canceledCtx
} else {
ctx = context.Background()
}
env := NewRequestEnvelope(ctx, "doSomethingLong", map[string]any{
"i": i,
"delay": delay,
})
t.Logf("publishing envelope #%d", i)
reply, err := b.Request(ctx, env)
if err != nil {
if errors.Is(err, context.Canceled) && isCanceled {
return
}
if errors.Is(err, bus.ErrNoResponse) && isCanceled {
return
}
t.Errorf("%+v", errors.WithStack(err))
return
}
result, ok := reply.Message().(int64)
if !ok {
t.Errorf("response.Result: expected type '%T', got '%T'", int64(0), reply.Message())
return
}
if e, g := i, int(result); e != g {
t.Errorf("response.Result: expected '%v', got '%v'", e, g)
return
}
}(i)
}
wg.Wait()
}

14
pkg/module/rpc/testdata/deadlock.js vendored Normal file
View File

@ -0,0 +1,14 @@
function onInit() {
rpc.register("doSomethingLong", doSomethingLong)
}
function doSomethingLong(ctx, params) {
var start = Date.now()
while (true) {
var now = Date.now()
if (now - start >= params.delay) break
}
return params.i;
}

View File

@ -33,18 +33,14 @@ func TestModule(t *testing.T) {
t.Fatalf("%+v", errors.WithStack(err))
}
if err := server.Load("testdata/share.js", string(data)); err != nil {
ctx := context.Background()
if err := server.Start(ctx, "testdata/share.js", string(data)); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
ctx := context.Background()
if err := server.Start(ctx); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
defer server.Stop()
if _, err := server.ExecFuncByName(context.Background(), "testModule"); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
server.Stop()
}

View File

@ -27,18 +27,14 @@ func TestStoreModule(t *testing.T) {
t.Fatalf("%+v", errors.WithStack(err))
}
if err := server.Load("testdata/store.js", string(data)); err != nil {
ctx := context.Background()
if err := server.Start(ctx, "testdata/store.js", string(data)); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
ctx := context.Background()
if err := server.Start(ctx); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
defer server.Stop()
if _, err := server.ExecFuncByName(context.Background(), "testStore"); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
server.Stop()
}