Add force=restart parameter for process updates

This commit is contained in:
Ingo Oppermann
2025-07-18 15:47:49 +02:00
parent bcc3c9aaa2
commit 0e38648b70
17 changed files with 66 additions and 51 deletions

View File

@@ -516,7 +516,7 @@ func (a *api) ProcessUpdate(c echo.Context) error {
"new_id": r.Config.ProcessID(),
}).Log("Update process request")
err := a.cluster.ProcessUpdate(origin, pid, &r.Config)
err := a.cluster.ProcessUpdate(origin, pid, &r.Config, r.Force)
if err != nil {
a.logger.Debug().WithError(err).WithField("id", pid).Log("Unable to update process")
return ErrFromClusterError(err)

View File

@@ -33,6 +33,7 @@ type GetProcessResponse struct {
type UpdateProcessRequest struct {
Config app.Config `json:"config"`
Force bool `json:"force"`
}
type SetProcessCommandRequest struct {

View File

@@ -61,7 +61,7 @@ type Cluster interface {
ProcessAdd(origin string, config *app.Config) error
ProcessGet(origin string, id app.ProcessID, stale bool) (store.Process, string, error)
ProcessRemove(origin string, id app.ProcessID) error
ProcessUpdate(origin string, id app.ProcessID, config *app.Config) error
ProcessUpdate(origin string, id app.ProcessID, config *app.Config, force bool) error
ProcessSetCommand(origin string, id app.ProcessID, order string) error
ProcessSetMetadata(origin string, id app.ProcessID, key string, data interface{}) error
ProcessGetMetadata(origin string, id app.ProcessID, key string) (interface{}, error)

View File

@@ -36,13 +36,14 @@ func (f *Forwarder) ProcessGet(origin string, id app.ProcessID) (store.Process,
return process, nodeid, reconstructError(err)
}
func (f *Forwarder) ProcessUpdate(origin string, id app.ProcessID, config *app.Config) error {
func (f *Forwarder) ProcessUpdate(origin string, id app.ProcessID, config *app.Config, force bool) error {
if origin == "" {
origin = f.ID
}
r := apiclient.UpdateProcessRequest{
Config: *config,
Force: force,
}
f.lock.RLock()

View File

@@ -470,7 +470,7 @@ type processOpStop struct {
type processOpAdd struct {
nodeid string
config *app.Config
metadata map[string]interface{}
metadata map[string]any
order string
}
@@ -478,7 +478,8 @@ type processOpUpdate struct {
nodeid string
processid app.ProcessID
config *app.Config
metadata map[string]interface{}
metadata map[string]any
force bool
}
type processOpReject struct {
@@ -594,7 +595,7 @@ func (c *cluster) applyOp(op interface{}, logger log.Logger) processOpError {
"nodeid": v.nodeid,
}).Log("Adding process")
case processOpUpdate:
err := c.manager.ProcessUpdate(v.nodeid, v.processid, v.config, v.metadata)
err := c.manager.ProcessUpdate(v.nodeid, v.processid, v.config, v.metadata, v.force)
if err != nil {
opErr = processOpError{
processid: v.processid,

View File

@@ -189,6 +189,9 @@ func synchronize(wish map[string]string, want []store.Process, have []node.Proce
// The process is on the wantMap. Update the process if the configuration and/or metadata differ.
hasConfigChanges := !wantP.Config.Equal(haveP.Config)
if !hasConfigChanges && wantP.Force {
hasConfigChanges = wantP.UpdatedAt.After(haveP.UpdatedAt)
}
hasMetadataChanges, metadata := isMetadataUpdateRequired(wantP.Metadata, haveP.Metadata)
if opBudget > 0 {
if hasConfigChanges || hasMetadataChanges {
@@ -200,6 +203,7 @@ func synchronize(wish map[string]string, want []store.Process, have []node.Proce
processid: haveP.Config.ProcessID(),
config: wantP.Config,
metadata: metadata,
force: wantP.Force,
})
opBudget -= 3

View File

@@ -349,7 +349,7 @@ func (n *Core) ProcessDelete(id app.ProcessID) error {
return client.ProcessDelete(id)
}
func (n *Core) ProcessUpdate(id app.ProcessID, config *app.Config, metadata map[string]interface{}) error {
func (n *Core) ProcessUpdate(id app.ProcessID, config *app.Config, metadata map[string]any, force bool) error {
n.lock.RLock()
client := n.client
n.lock.RUnlock()
@@ -358,7 +358,7 @@ func (n *Core) ProcessUpdate(id app.ProcessID, config *app.Config, metadata map[
return ErrNoPeer
}
return client.ProcessUpdate(id, config, metadata)
return client.ProcessUpdate(id, config, metadata, force)
}
func (n *Core) ProcessReportSet(id app.ProcessID, report *app.Report) error {

View File

@@ -563,7 +563,7 @@ func (p *Manager) ProcessGet(nodeid string, id app.ProcessID, filter []string) (
return process, nil
}
func (p *Manager) ProcessAdd(nodeid string, config *app.Config, metadata map[string]interface{}) error {
func (p *Manager) ProcessAdd(nodeid string, config *app.Config, metadata map[string]any) error {
node, err := p.NodeGet(nodeid)
if err != nil {
return err
@@ -581,13 +581,13 @@ func (p *Manager) ProcessDelete(nodeid string, id app.ProcessID) error {
return node.Core().ProcessDelete(id)
}
func (p *Manager) ProcessUpdate(nodeid string, id app.ProcessID, config *app.Config, metadata map[string]interface{}) error {
func (p *Manager) ProcessUpdate(nodeid string, id app.ProcessID, config *app.Config, metadata map[string]any, force bool) error {
node, err := p.NodeGet(nodeid)
if err != nil {
return err
}
return node.Core().ProcessUpdate(id, config, metadata)
return node.Core().ProcessUpdate(id, config, metadata, force)
}
func (p *Manager) ProcessReportSet(nodeid string, id app.ProcessID, report *app.Report) error {

View File

@@ -58,9 +58,9 @@ func (c *cluster) ProcessRemove(origin string, id app.ProcessID) error {
return c.applyCommand(cmd)
}
func (c *cluster) ProcessUpdate(origin string, id app.ProcessID, config *app.Config) error {
func (c *cluster) ProcessUpdate(origin string, id app.ProcessID, config *app.Config, force bool) error {
if !c.IsRaftLeader() {
return c.forwarder.ProcessUpdate(origin, id, config)
return c.forwarder.ProcessUpdate(origin, id, config, force)
}
nodeid := c.manager.GetRandomNode()
@@ -74,6 +74,7 @@ func (c *cluster) ProcessUpdate(origin string, id app.ProcessID, config *app.Con
Data: &store.CommandUpdateProcess{
ID: id,
Config: config,
Force: force,
},
}

View File

@@ -74,12 +74,9 @@ func (s *store) updateProcess(cmd CommandUpdateProcess) error {
}
if srcid == dstid {
if p.Config.Equal(cmd.Config) {
return nil
}
p.UpdatedAt = time.Now()
p.Config = cmd.Config
p.Force = cmd.Force
s.data.Process[srcid] = p
@@ -214,6 +211,7 @@ func (s *store) ProcessList() []Process {
Order: p.Order,
Metadata: p.Metadata,
Error: p.Error,
Force: p.Force,
})
}

View File

@@ -45,8 +45,9 @@ type Process struct {
UpdatedAt time.Time
Config *app.Config
Order string
Metadata map[string]interface{}
Metadata map[string]any
Error string
Force bool
}
type Users struct {
@@ -109,6 +110,7 @@ type CommandRemoveProcess struct {
type CommandUpdateProcess struct {
ID app.ProcessID
Config *app.Config
Force bool
}
type CommandSetRelocateProcess struct {

View File

@@ -59,21 +59,21 @@ type RestClient interface {
Events(ctx context.Context, filters api.EventFilters) (<-chan api.Event, error) // POST /v3/events
ProcessList(opts ProcessListOptions) ([]api.Process, error) // GET /v3/process
ProcessAdd(p *app.Config, metadata map[string]interface{}) error // POST /v3/process
Process(id app.ProcessID, filter []string) (api.Process, error) // GET /v3/process/{id}
ProcessUpdate(id app.ProcessID, p *app.Config, metadata map[string]interface{}) error // PUT /v3/process/{id}
ProcessDelete(id app.ProcessID) error // DELETE /v3/process/{id}
ProcessCommand(id app.ProcessID, command string) error // PUT /v3/process/{id}/command
ProcessProbe(id app.ProcessID) (api.Probe, error) // GET /v3/process/{id}/probe
ProcessProbeConfig(config *app.Config) (api.Probe, error) // POST /v3/process/probe
ProcessValidateConfig(p *app.Config) error // POST /v3/process/validate
ProcessConfig(id app.ProcessID) (api.ProcessConfig, error) // GET /v3/process/{id}/config
ProcessReport(id app.ProcessID) (api.ProcessReport, error) // GET /v3/process/{id}/report
ProcessReportSet(id app.ProcessID, report *app.Report) error // PUT /v3/process/{id}/report
ProcessState(id app.ProcessID) (api.ProcessState, error) // GET /v3/process/{id}/state
ProcessMetadata(id app.ProcessID, key string) (api.Metadata, error) // GET /v3/process/{id}/metadata/{key}
ProcessMetadataSet(id app.ProcessID, key string, metadata api.Metadata) error // PUT /v3/process/{id}/metadata/{key}
ProcessList(opts ProcessListOptions) ([]api.Process, error) // GET /v3/process
ProcessAdd(p *app.Config, metadata map[string]any) error // POST /v3/process
Process(id app.ProcessID, filter []string) (api.Process, error) // GET /v3/process/{id}
ProcessUpdate(id app.ProcessID, p *app.Config, metadata map[string]any, force bool) error // PUT /v3/process/{id}
ProcessDelete(id app.ProcessID) error // DELETE /v3/process/{id}
ProcessCommand(id app.ProcessID, command string) error // PUT /v3/process/{id}/command
ProcessProbe(id app.ProcessID) (api.Probe, error) // GET /v3/process/{id}/probe
ProcessProbeConfig(config *app.Config) (api.Probe, error) // POST /v3/process/probe
ProcessValidateConfig(p *app.Config) error // POST /v3/process/validate
ProcessConfig(id app.ProcessID) (api.ProcessConfig, error) // GET /v3/process/{id}/config
ProcessReport(id app.ProcessID) (api.ProcessReport, error) // GET /v3/process/{id}/report
ProcessReportSet(id app.ProcessID, report *app.Report) error // PUT /v3/process/{id}/report
ProcessState(id app.ProcessID) (api.ProcessState, error) // GET /v3/process/{id}/state
ProcessMetadata(id app.ProcessID, key string) (api.Metadata, error) // GET /v3/process/{id}/metadata/{key}
ProcessMetadataSet(id app.ProcessID, key string, metadata api.Metadata) error // PUT /v3/process/{id}/metadata/{key}
RTMPChannels() ([]api.RTMPChannel, error) // GET /v3/rtmp
SRTChannels() ([]api.SRTChannel, error) // GET /v3/srt

View File

@@ -81,7 +81,7 @@ func (r *restclient) ProcessAdd(p *app.Config, metadata map[string]interface{})
return nil
}
func (r *restclient) ProcessUpdate(id app.ProcessID, p *app.Config, metadata map[string]interface{}) error {
func (r *restclient) ProcessUpdate(id app.ProcessID, p *app.Config, metadata map[string]any, force bool) error {
buf := mem.Get()
defer mem.Put(buf)
@@ -94,6 +94,10 @@ func (r *restclient) ProcessUpdate(id app.ProcessID, p *app.Config, metadata map
query := &url.Values{}
query.Set("domain", id.Domain)
if force {
query.Set("force", "restart")
}
_, err := r.call("PUT", "/v3/process/"+url.PathEscape(id.ID), query, nil, "application/json", buf.Reader())
if err != nil {
return err

View File

@@ -305,6 +305,7 @@ func (h *ClusterHandler) ProcessAdd(c echo.Context) error {
func (h *ClusterHandler) ProcessUpdate(c echo.Context) error {
ctxuser := util.DefaultContext(c, "user", "")
domain := util.DefaultQuery(c, "domain", "")
force := util.DefaultQuery(c, "force", "")
id := util.PathParam(c, "id")
process := api.ProcessConfig{
@@ -339,7 +340,7 @@ func (h *ClusterHandler) ProcessUpdate(c echo.Context) error {
config, metadata := process.Marshal()
if err := h.cluster.ProcessUpdate("", pid, config); err != nil {
if err := h.cluster.ProcessUpdate("", pid, config, force == "restart"); err != nil {
if err == restream.ErrUnknownProcess {
return api.Err(http.StatusNotFound, "", "process not found: %s in domain '%s'", pid.ID, pid.Domain)
}

View File

@@ -285,6 +285,7 @@ func (h *ProcessHandler) Delete(c echo.Context) error {
func (h *ProcessHandler) Update(c echo.Context) error {
ctxuser := util.DefaultContext(c, "user", "")
domain := util.DefaultQuery(c, "domain", "")
force := util.DefaultQuery(c, "force", "")
id := util.PathParam(c, "id")
process := api.ProcessConfig{
@@ -322,7 +323,7 @@ func (h *ProcessHandler) Update(c echo.Context) error {
config, metadata := process.Marshal()
if err := h.restream.UpdateProcess(tid, config); err != nil {
if err := h.restream.UpdateProcess(tid, config, force == "restart"); err != nil {
return h.apiErrorFromError(err)
}

View File

@@ -43,7 +43,7 @@ type Restreamer interface {
AddProcess(config *app.Config) error // Add a new process
GetProcessIDs(idpattern, refpattern, ownerpattern, domainpattern string) []app.ProcessID // Get a list of process IDs based on patterns for ID and reference
DeleteProcess(id app.ProcessID) error // Delete a process
UpdateProcess(id app.ProcessID, config *app.Config) error // Update a process
UpdateProcess(id app.ProcessID, config *app.Config, force bool) error // Update a process
StartProcess(id app.ProcessID) error // Start a process
StopProcess(id app.ProcessID) error // Stop a process
RestartProcess(id app.ProcessID) error // Restart a process
@@ -1091,15 +1091,16 @@ func (r *restream) resolveAddress(tasks *Storage, id, address string) (string, e
continue
}
if matches["source"] == "hls" {
switch matches["source"] {
case "hls":
if (u.Scheme == "http" || u.Scheme == "https") && strings.HasSuffix(u.RawPath, ".m3u8") {
return r.rewrite.RewriteAddress(a, t.config.Owner, rewrite.READ), nil
}
} else if matches["source"] == "rtmp" {
case "rtmp":
if u.Scheme == "rtmp" {
return r.rewrite.RewriteAddress(a, t.config.Owner, rewrite.READ), nil
}
} else if matches["source"] == "srt" {
case "srt":
if u.Scheme == "srt" {
return r.rewrite.RewriteAddress(a, t.config.Owner, rewrite.READ), nil
}
@@ -1161,7 +1162,7 @@ func parseAddressReference(address string) (map[string]string, error) {
return results, nil
}
func (r *restream) UpdateProcess(id app.ProcessID, config *app.Config) error {
func (r *restream) UpdateProcess(id app.ProcessID, config *app.Config, force bool) error {
task, ok := r.tasks.LoadAndLock(id)
if !ok {
return ErrUnknownProcess
@@ -1169,7 +1170,7 @@ func (r *restream) UpdateProcess(id app.ProcessID, config *app.Config) error {
defer r.tasks.Unlock(id)
err := r.updateProcess(task, config)
err := r.updateProcess(task, config, force)
if err != nil {
return err
@@ -1180,9 +1181,9 @@ func (r *restream) UpdateProcess(id app.ProcessID, config *app.Config) error {
return nil
}
func (r *restream) updateProcess(task *task, config *app.Config) error {
func (r *restream) updateProcess(task *task, config *app.Config, force bool) error {
// If the new config has the same hash as the current config, do nothing.
if task.Equal(config) {
if !force && task.Equal(config) {
return nil
}

View File

@@ -298,12 +298,12 @@ func TestUpdateProcess(t *testing.T) {
process3.ID = "process2"
tid3 := app.ProcessID{ID: process3.ID}
err = rs.UpdateProcess(tid1, process3)
err = rs.UpdateProcess(tid1, process3, false)
require.Error(t, err)
process3.ID = "process3"
tid3.ID = process3.ID
err = rs.UpdateProcess(tid1, process3)
err = rs.UpdateProcess(tid1, process3, false)
require.NoError(t, err)
_, err = rs.GetProcess(tid1)
@@ -335,7 +335,7 @@ func TestUpdateSameHashProcess(t *testing.T) {
time.Sleep(2 * time.Second)
err = rs.UpdateProcess(tid, config)
err = rs.UpdateProcess(tid, config, false)
require.NoError(t, err)
process, err = rs.GetProcess(tid)
@@ -374,7 +374,7 @@ func TestUpdateProcessLogHistoryTransfer(t *testing.T) {
require.NotNil(t, p)
p.ID = "process2"
err = rs.UpdateProcess(tid1, p)
err = rs.UpdateProcess(tid1, p, false)
require.NoError(t, err)
tid2 := app.ProcessID{ID: p.ID}
@@ -419,7 +419,7 @@ func TestUpdateProcessMetadataTransfer(t *testing.T) {
require.NotNil(t, p)
p.ID = "process2"
err = rs.UpdateProcess(tid1, p)
err = rs.UpdateProcess(tid1, p, false)
require.NoError(t, err)
tid2 := app.ProcessID{ID: p.ID}
@@ -836,7 +836,7 @@ func TestLogTransfer(t *testing.T) {
require.Equal(t, 1, len(log.History))
err = rs.UpdateProcess(tid, process)
err = rs.UpdateProcess(tid, process, false)
require.NoError(t, err)
log, _ = rs.GetProcessReport(tid)
@@ -1858,7 +1858,7 @@ func TestProcessCleanup(t *testing.T) {
require.Equal(t, 10, len(files))
process.Reference = "foobar"
rsi.UpdateProcess(app.ProcessID{ID: process.ID}, process)
rsi.UpdateProcess(app.ProcessID{ID: process.ID}, process, false)
rsi.Start()