feat: rewrite bus to prevent deadlocks
This commit is contained in:
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
55
pkg/module/blob/envelope.go
Normal file
55
pkg/module/blob/envelope.go
Normal 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
212
pkg/module/blob/http.go
Normal 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{}
|
||||
)
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
|
38
pkg/module/fetch/envelope.go
Normal file
38
pkg/module/fetch/envelope.go
Normal 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,
|
||||
})
|
||||
}
|
@ -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
115
pkg/module/fetch/http.go
Normal 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))
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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{}
|
21
pkg/module/rpc/envelope.go
Normal file
21
pkg/module/rpc/envelope.go
Normal 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
7
pkg/module/rpc/error.go
Normal file
@ -0,0 +1,7 @@
|
||||
package rpc
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrMethodNotFound = errors.New("method not found")
|
||||
)
|
19
pkg/module/rpc/jsonrpc.go
Normal file
19
pkg/module/rpc/jsonrpc.go
Normal 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
256
pkg/module/rpc/module.go
Normal 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{}
|
109
pkg/module/rpc/module_test.go
Normal file
109
pkg/module/rpc/module_test.go
Normal 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
14
pkg/module/rpc/testdata/deadlock.js
vendored
Normal 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;
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
Reference in New Issue
Block a user