Add process storage in raft

This commit is contained in:
Ingo Oppermann
2023-05-05 17:31:57 +02:00
parent f4015f5cbd
commit b8b2990e61
7 changed files with 292 additions and 25 deletions

View File

@@ -17,6 +17,7 @@ import (
mwlog "github.com/datarhei/core/v16/http/middleware/log" mwlog "github.com/datarhei/core/v16/http/middleware/log"
"github.com/datarhei/core/v16/http/validator" "github.com/datarhei/core/v16/http/validator"
"github.com/datarhei/core/v16/log" "github.com/datarhei/core/v16/log"
"github.com/datarhei/core/v16/restream/app"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware" "github.com/labstack/echo/v4/middleware"
@@ -52,6 +53,16 @@ type LeaveRequest struct {
ID string `json:"id"` ID string `json:"id"`
} }
type AddProcessRequest struct {
Origin string `json:"origin"`
Config app.Config `json:"config"`
}
type RemoveProcessRequest struct {
Origin string `json:"origin"`
ID string `json:"id"`
}
func NewAPI(config APIConfig) (API, error) { func NewAPI(config APIConfig) (API, error) {
a := &api{ a := &api{
id: config.ID, id: config.ID,
@@ -150,11 +161,47 @@ func NewAPI(config APIConfig) (API, error) {
}) })
a.router.POST("/v1/process", func(c echo.Context) error { a.router.POST("/v1/process", func(c echo.Context) error {
return httpapi.Err(http.StatusNotImplemented, "") r := AddProcessRequest{}
if err := util.ShouldBindJSON(c, &r); err != nil {
return httpapi.Err(http.StatusBadRequest, "Invalid JSON", "%s", err)
}
a.logger.Debug().WithField("id", r.Config.ID).Log("got add process request")
if r.Origin == a.id {
return httpapi.Err(http.StatusLoopDetected, "", "breaking circuit")
}
err := a.cluster.AddProcess(r.Origin, &r.Config)
if err != nil {
a.logger.Debug().WithError(err).WithField("id", r.Config.ID).Log("unable to add process")
return httpapi.Err(http.StatusInternalServerError, "unable to add process", "%s", err)
}
return c.JSON(http.StatusOK, "OK")
}) })
a.router.DELETE("/v1/process/:id", func(c echo.Context) error { a.router.POST("/v1/process/:id", func(c echo.Context) error {
return httpapi.Err(http.StatusNotImplemented, "") r := RemoveProcessRequest{}
if err := util.ShouldBindJSON(c, &r); err != nil {
return httpapi.Err(http.StatusBadRequest, "Invalid JSON", "%s", err)
}
a.logger.Debug().WithField("id", r.ID).Log("got remove process request")
if r.Origin == a.id {
return httpapi.Err(http.StatusLoopDetected, "", "breaking circuit")
}
err := a.cluster.RemoveProcess(r.Origin, r.ID)
if err != nil {
a.logger.Debug().WithError(err).WithField("id", r.ID).Log("unable to remove process")
return httpapi.Err(http.StatusInternalServerError, "unable to remove process", "%s", err)
}
return c.JSON(http.StatusOK, "OK")
}) })
a.router.GET("/v1/core", func(c echo.Context) error { a.router.GET("/v1/core", func(c echo.Context) error {
@@ -216,6 +263,28 @@ func (c *APIClient) Leave(r LeaveRequest) error {
return err return err
} }
func (c *APIClient) AddProcess(r AddProcessRequest) error {
data, err := json.Marshal(r)
if err != nil {
return err
}
_, err = c.call(http.MethodPost, "/process", "application/json", bytes.NewReader(data))
return err
}
func (c *APIClient) RemoveProcess(r RemoveProcessRequest) error {
data, err := json.Marshal(r)
if err != nil {
return err
}
_, err = c.call(http.MethodPost, "/process/"+r.ID, "application/json", bytes.NewReader(data))
return err
}
func (c *APIClient) Snapshot() (io.ReadCloser, error) { func (c *APIClient) Snapshot() (io.ReadCloser, error) {
return c.stream(http.MethodGet, "/snapshot", "", nil) return c.stream(http.MethodGet, "/snapshot", "", nil)
} }

View File

@@ -72,6 +72,9 @@ type Cluster interface {
ListNodes() []addNodeCommand ListNodes() []addNodeCommand
GetNode(id string) (addNodeCommand, error) GetNode(id string) (addNodeCommand, error)
AddProcess(origin string, config *app.Config) error
RemoveProcess(origin, id string) error
ProxyReader() ProxyReader ProxyReader() ProxyReader
} }
@@ -976,15 +979,30 @@ func (c *cluster) followerLoop(stopCh chan struct{}) {
} }
} }
func (c *cluster) AddProcess(config app.Config) error { func (c *cluster) AddProcess(origin string, config *app.Config) error {
if !c.IsRaftLeader() { if !c.IsRaftLeader() {
return c.forwarder.AddProcess() return c.forwarder.AddProcess(origin, config)
} }
cmd := &command{ cmd := &command{
Operation: "addProcess", Operation: opAddProcess,
Data: &addProcessCommand{ Data: &addProcessCommand{
Config: nil, Config: *config,
},
}
return c.applyCommand(cmd)
}
func (c *cluster) RemoveProcess(origin, id string) error {
if !c.IsRaftLeader() {
return c.forwarder.RemoveProcess(origin, id)
}
cmd := &command{
Operation: opRemoveProcess,
Data: &removeProcessCommand{
ID: id,
}, },
} }

View File

@@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/datarhei/core/v16/log" "github.com/datarhei/core/v16/log"
"github.com/datarhei/core/v16/restream/app"
) )
// Forwarder forwards any HTTP request from a follower to the leader // Forwarder forwards any HTTP request from a follower to the leader
@@ -17,9 +18,9 @@ type Forwarder interface {
Join(origin, id, raftAddress, peerAddress string) error Join(origin, id, raftAddress, peerAddress string) error
Leave(origin, id string) error Leave(origin, id string) error
Snapshot() (io.ReadCloser, error) Snapshot() (io.ReadCloser, error)
AddProcess() error AddProcess(origin string, config *app.Config) error
UpdateProcess() error UpdateProcess() error
RemoveProcess() error RemoveProcess(origin, id string) error
} }
type forwarder struct { type forwarder struct {
@@ -134,14 +135,40 @@ func (f *forwarder) Snapshot() (io.ReadCloser, error) {
return client.Snapshot() return client.Snapshot()
} }
func (f *forwarder) AddProcess() error { func (f *forwarder) AddProcess(origin string, config *app.Config) error {
return fmt.Errorf("not implemented") if origin == "" {
origin = f.id
}
r := AddProcessRequest{
Origin: origin,
Config: *config,
}
f.lock.RLock()
client := f.client
f.lock.RUnlock()
return client.AddProcess(r)
} }
func (f *forwarder) UpdateProcess() error { func (f *forwarder) UpdateProcess() error {
return fmt.Errorf("not implemented") return fmt.Errorf("not implemented")
} }
func (f *forwarder) RemoveProcess() error { func (f *forwarder) RemoveProcess(origin, id string) error {
return fmt.Errorf("not implemented") if origin == "" {
origin = f.id
}
r := RemoveProcessRequest{
Origin: origin,
ID: id,
}
f.lock.RLock()
client := f.client
f.lock.RUnlock()
return client.RemoveProcess(r)
} }

View File

@@ -26,6 +26,12 @@ type Node interface {
GetURL(path string) (string, error) GetURL(path string) (string, error)
GetFile(path string) (io.ReadCloser, error) GetFile(path string) (io.ReadCloser, error)
ProcessGetAll() ([]string, error)
ProcessSet() error
ProcessStart(id string) error
ProcessStop(id string) error
ProcessDelete(id string) error
NodeReader NodeReader
} }
@@ -463,3 +469,34 @@ func (n *node) GetFile(path string) (io.ReadCloser, error) {
return nil, fmt.Errorf("unknown prefix") return nil, fmt.Errorf("unknown prefix")
} }
func (n *node) ProcessGetAll() ([]string, error) {
processes, err := n.peer.ProcessList(nil, nil)
if err != nil {
return nil, err
}
list := []string{}
for _, p := range processes {
list = append(list, p.ID)
}
return list, nil
}
func (n *node) ProcessSet() error {
return nil
}
func (n *node) ProcessStart(id string) error {
return n.peer.ProcessCommand(id, "start")
}
func (n *node) ProcessStop(id string) error {
return n.peer.ProcessCommand(id, "stop")
}
func (n *node) ProcessDelete(id string) error {
return n.peer.ProcessDelete(id)
}

View File

@@ -6,6 +6,8 @@ import (
"io" "io"
"sync" "sync"
"github.com/datarhei/core/v16/restream/app"
"github.com/hashicorp/raft" "github.com/hashicorp/raft"
) )
@@ -14,6 +16,9 @@ type Store interface {
ListNodes() []StoreNode ListNodes() []StoreNode
GetNode(id string) (StoreNode, error) GetNode(id string) (StoreNode, error)
ListProcesses() []app.Config
GetProcess(id string) (app.Config, error)
} }
type operation string type operation string
@@ -45,18 +50,24 @@ type StoreNode struct {
} }
type addProcessCommand struct { type addProcessCommand struct {
Config []byte app.Config
}
type removeProcessCommand struct {
ID string
} }
// Implement a FSM // Implement a FSM
type store struct { type store struct {
lock sync.RWMutex lock sync.RWMutex
Nodes map[string]string Nodes map[string]string
Process map[string]app.Config
} }
func NewStore() (Store, error) { func NewStore() (Store, error) {
return &store{ return &store{
Nodes: map[string]string{}, Nodes: map[string]string{},
Process: map[string]app.Config{},
}, nil }, nil
} }
@@ -95,10 +106,26 @@ func (s *store) Apply(log *raft.Log) interface{} {
s.lock.Lock() s.lock.Lock()
delete(s.Nodes, cmd.ID) delete(s.Nodes, cmd.ID)
s.lock.Unlock() s.lock.Unlock()
case opAddProcess:
b, _ := json.Marshal(c.Data)
cmd := addProcessCommand{}
json.Unmarshal(b, &cmd)
s.lock.Lock()
s.Process[cmd.ID] = cmd.Config
s.lock.Unlock()
case opRemoveProcess:
b, _ := json.Marshal(c.Data)
cmd := removeProcessCommand{}
json.Unmarshal(b, &cmd)
s.lock.Lock()
delete(s.Process, cmd.ID)
s.lock.Unlock()
} }
s.lock.RLock() s.lock.RLock()
fmt.Printf("\n==> %+v\n\n", s.Nodes) fmt.Printf("\n==> %+v\n\n", s.Process)
s.lock.RUnlock() s.lock.RUnlock()
return nil return nil
} }
@@ -166,6 +193,31 @@ func (s *store) GetNode(id string) (StoreNode, error) {
}, nil }, nil
} }
func (s *store) ListProcesses() []app.Config {
s.lock.RLock()
defer s.lock.RUnlock()
processes := []app.Config{}
for _, cfg := range s.Process {
processes = append(processes, *cfg.Clone())
}
return processes
}
func (s *store) GetProcess(id string) (app.Config, error) {
s.lock.RLock()
defer s.lock.RUnlock()
cfg, ok := s.Process[id]
if !ok {
return app.Config{}, fmt.Errorf("not found")
}
return *cfg.Clone(), nil
}
type fsmSnapshot struct { type fsmSnapshot struct {
data []byte data []byte
} }

View File

@@ -10,6 +10,7 @@ import (
"github.com/datarhei/core/v16/http/api" "github.com/datarhei/core/v16/http/api"
"github.com/datarhei/core/v16/http/handler/util" "github.com/datarhei/core/v16/http/handler/util"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/lithammer/shortuuid/v4"
) )
// The ClusterHandler type provides handler functions for manipulating the cluster config. // The ClusterHandler type provides handler functions for manipulating the cluster config.
@@ -31,6 +32,7 @@ func NewCluster(cluster cluster.Cluster) *ClusterHandler {
// GetProxyNodes returns the list of proxy nodes in the cluster // GetProxyNodes returns the list of proxy nodes in the cluster
// @Summary List of proxy nodes in the cluster // @Summary List of proxy nodes in the cluster
// @Description List of proxy nodes in the cluster // @Description List of proxy nodes in the cluster
// @Tags v16.?.?
// @ID cluster-3-get-proxy-nodes // @ID cluster-3-get-proxy-nodes
// @Produce json // @Produce json
// @Success 200 {array} api.ClusterNode // @Success 200 {array} api.ClusterNode
@@ -62,6 +64,7 @@ func (h *ClusterHandler) GetProxyNodes(c echo.Context) error {
// GetProxyNode returns the proxy node with the given ID // GetProxyNode returns the proxy node with the given ID
// @Summary List a proxy node by its ID // @Summary List a proxy node by its ID
// @Description List a proxy node by its ID // @Description List a proxy node by its ID
// @Tags v16.?.?
// @ID cluster-3-get-proxy-node // @ID cluster-3-get-proxy-node
// @Produce json // @Produce json
// @Param id path string true "Node ID" // @Param id path string true "Node ID"
@@ -92,6 +95,7 @@ func (h *ClusterHandler) GetProxyNode(c echo.Context) error {
// GetProxyNodeFiles returns the files from the proxy node with the given ID // GetProxyNodeFiles returns the files from the proxy node with the given ID
// @Summary List the files of a proxy node by its ID // @Summary List the files of a proxy node by its ID
// @Description List the files of a proxy node by its ID // @Description List the files of a proxy node by its ID
// @Tags v16.?.?
// @ID cluster-3-get-proxy-node-files // @ID cluster-3-get-proxy-node-files
// @Produce json // @Produce json
// @Param id path string true "Node ID" // @Param id path string true "Node ID"
@@ -126,6 +130,7 @@ func (h *ClusterHandler) GetProxyNodeFiles(c echo.Context) error {
// GetCluster returns the list of nodes in the cluster // GetCluster returns the list of nodes in the cluster
// @Summary List of nodes in the cluster // @Summary List of nodes in the cluster
// @Description List of nodes in the cluster // @Description List of nodes in the cluster
// @Tags v16.?.?
// @ID cluster-3-get-cluster // @ID cluster-3-get-cluster
// @Produce json // @Produce json
// @Success 200 {object} api.ClusterAbout // @Success 200 {object} api.ClusterAbout
@@ -160,6 +165,67 @@ func (h *ClusterHandler) About(c echo.Context) error {
return c.JSON(http.StatusOK, about) return c.JSON(http.StatusOK, about)
} }
// Add adds a new process to the cluster
// @Summary Add a new process
// @Description Add a new FFmpeg process
// @Tags v16.?.?
// @ID cluster-3-add-process
// @Accept json
// @Produce json
// @Param config body api.ProcessConfig true "Process config"
// @Success 200 {object} api.ProcessConfig
// @Failure 400 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/cluster/process [post]
func (h *ClusterHandler) AddProcess(c echo.Context) error {
process := api.ProcessConfig{
ID: shortuuid.New(),
Type: "ffmpeg",
Autostart: true,
}
if err := util.ShouldBindJSON(c, &process); err != nil {
return api.Err(http.StatusBadRequest, "Invalid JSON", "%s", err)
}
if process.Type != "ffmpeg" {
return api.Err(http.StatusBadRequest, "Unsupported process type", "Supported process types are: ffmpeg")
}
if len(process.Input) == 0 || len(process.Output) == 0 {
return api.Err(http.StatusBadRequest, "At least one input and one output need to be defined")
}
config := process.Marshal()
if err := h.cluster.AddProcess("", config); err != nil {
return api.Err(http.StatusBadRequest, "Invalid process config", "%s", err.Error())
}
return c.JSON(http.StatusOK, process)
}
// Delete deletes the process with the given ID from the cluster
// @Summary Delete a process by its ID
// @Description Delete a process by its ID
// @Tags v16.?.?
// @ID cluster-3-delete-process
// @Produce json
// @Param id path string true "Process ID"
// @Success 200 {string} string
// @Failure 404 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/cluster/process/{id} [delete]
func (h *ClusterHandler) DeleteProcess(c echo.Context) error {
id := util.PathParam(c, "id")
if err := h.cluster.RemoveProcess("", id); err != nil {
return api.Err(http.StatusInternalServerError, "Process can't be deleted", "%s", err)
}
return c.JSON(http.StatusOK, "OK")
}
/* /*
// AddNode adds a new node // AddNode adds a new node
// @Summary Add a new node // @Summary Add a new node

View File

@@ -664,13 +664,11 @@ func (s *server) setRoutesV3(v3 *echo.Group) {
v3.GET("/cluster/proxy", s.v3handler.cluster.GetProxyNodes) v3.GET("/cluster/proxy", s.v3handler.cluster.GetProxyNodes)
v3.GET("/cluster/proxy/node/:id", s.v3handler.cluster.GetProxyNode) v3.GET("/cluster/proxy/node/:id", s.v3handler.cluster.GetProxyNode)
v3.GET("/cluster/proxy/node/:id/files", s.v3handler.cluster.GetProxyNodeFiles) v3.GET("/cluster/proxy/node/:id/files", s.v3handler.cluster.GetProxyNodeFiles)
/*
if !s.readOnly { if !s.readOnly {
v3.POST("/cluster/node", s.v3handler.cluster.AddNode) v3.POST("/cluster/process", s.v3handler.cluster.AddProcess)
v3.PUT("/cluster/node/:id", s.v3handler.cluster.UpdateNode) v3.DELETE("/cluster/process/:id", s.v3handler.cluster.DeleteProcess)
v3.DELETE("/cluster/node/:id", s.v3handler.cluster.DeleteNode) }
}
*/
} }
// v3 Log // v3 Log