diff --git a/src/loader/mutators.go b/src/loader/mutators.go index e9df1f99..f1bed9bf 100644 --- a/src/loader/mutators.go +++ b/src/loader/mutators.go @@ -98,6 +98,7 @@ func copyWorkingDirToProbes(p *types.Project) { } func cloneReplicas(p *types.Project) { + groupReplicas := make(map[string][]string) procsToAdd := make([]types.ProcessConfig, 0) procsToDel := make([]string, 0) for name, proc := range p.Processes { @@ -109,6 +110,7 @@ func cloneReplicas(p *types.Project) { newProc.ReplicaNum = replica repName := newProc.CalculateReplicaName() newProc.ReplicaName = repName + groupReplicas[name] = append(groupReplicas[name], repName) if proc.Replicas == 1 { // Even if replicas == 1, we use newProc to ensure // it has its own memory separate from any other references. @@ -124,6 +126,28 @@ func cloneReplicas(p *types.Project) { for _, proc := range procsToAdd { p.Processes[proc.ReplicaName] = proc } + + // dependency rewrite when replicas are present + for name, proc := range p.Processes { + if len(proc.DependsOn) == 0 { + continue + } + newDependsOn := make(types.DependsOnConfig) + for depName, depConf := range proc.DependsOn { + if replicas, ok := groupReplicas[depName]; ok && len(replicas) > 1 { + for _, replicaName := range replicas { + // if the user explicitly addressed this replica, dont overwrite it + if _, exists := proc.DependsOn[replicaName]; !exists { + newDependsOn[replicaName] = cloneDependency(depConf) + } + } + } else { + newDependsOn[depName] = cloneDependency(depConf) + } + } + proc.DependsOn = newDependsOn + p.Processes[name] = proc + } } func cloneProcess(proc *types.ProcessConfig) *types.ProcessConfig { @@ -131,9 +155,12 @@ func cloneProcess(proc *types.ProcessConfig) *types.ProcessConfig { newProc := *proc // 2. DEEP COPY the Vars Map - maps.Copy(newProc.Vars, proc.Vars) + newProc.Vars = maps.Clone(proc.Vars) - // 3. DEEP COPY the Environment Slices + // 3. DEEP COPY the DependsOn Map + newProc.DependsOn = cloneDependencies(proc.DependsOn) + + // 4. DEEP COPY the Environment Slices newProc.Environment = slices.Clone(proc.Environment) newProc.Args = slices.Clone(proc.Args) newProc.Entrypoint = slices.Clone(proc.Entrypoint) @@ -141,6 +168,25 @@ func cloneProcess(proc *types.ProcessConfig) *types.ProcessConfig { return &newProc } +func cloneDependencies(deps types.DependsOnConfig) types.DependsOnConfig { + if deps == nil { + return nil + } + newDeps := make(types.DependsOnConfig, len(deps)) + for k, v := range deps { + newDeps[k] = cloneDependency(v) + } + return newDeps +} + +func cloneDependency(dep types.ProcessDependency) types.ProcessDependency { + newDep := dep + if dep.Extensions != nil { + newDep.Extensions = maps.Clone(dep.Extensions) + } + return newDep +} + func assignExecutableAndArgs(p *types.Project) { elevatedShellArg := p.GetElevatedShellArg() for name, proc := range p.Processes { diff --git a/src/loader/mutators_test.go b/src/loader/mutators_test.go index cc44a5c6..255a795d 100644 --- a/src/loader/mutators_test.go +++ b/src/loader/mutators_test.go @@ -288,6 +288,96 @@ func Test_cloneReplicas(t *testing.T) { } } +func Test_cloneReplicas_DependsOn(t *testing.T) { + p := &types.Project{ + Processes: types.Processes{ + "p0": { + Name: "p0", + }, + "p1": { + Name: "p1", + Replicas: 2, + }, + "t0": { + Name: "t0", + DependsOn: types.DependsOnConfig{ + "p0": {Condition: types.ProcessConditionStarted}, + }, + }, + "t1": { + Name: "t1", + DependsOn: types.DependsOnConfig{ + "p1": {Condition: types.ProcessConditionHealthy}, + }, + }, + "t2": { + Name: "t2", + DependsOn: types.DependsOnConfig{ + "p0": {Condition: types.ProcessConditionStarted}, + "p1-0": {Condition: types.ProcessConditionHealthy}, + }, + }, + "t3": { + Name: "t3", + DependsOn: types.DependsOnConfig{ + "p1": {Condition: types.ProcessConditionStarted}, + "p1-1": {Condition: types.ProcessConditionHealthy}, + }, + }, + }, + } + assignDefaultProcessValues(p) + cloneReplicas(p) + + // depends on p0, no expansion needed + if len(p.Processes["t0"].DependsOn) != 1 { + t.Errorf("t0 should depend on a single process") + } + if _, ok := p.Processes["t0"].DependsOn["p0"]; !ok { + t.Errorf("t0 should depend on p0") + } + + // depends on p1, expansion is needed + if len(p.Processes["t1"].DependsOn) != 2 { + t.Errorf("t1 should depend on exactly 2 processes") + } + if _, ok := p.Processes["t1"].DependsOn["p1-0"]; !ok { + t.Errorf("t1 should depend on p1-0") + } + if _, ok := p.Processes["t1"].DependsOn["p1-1"]; !ok { + t.Errorf("t1 should depend on p1-1") + } + + // depends on p0 and p1-0, explicitly + if len(p.Processes["t2"].DependsOn) != 2 { + t.Errorf("t2 should depend on exactly 2 processes") + } + if _, ok := p.Processes["t2"].DependsOn["p0"]; !ok { + t.Errorf("t2 should depend on p0") + } + if p.Processes["t2"].DependsOn["p0"].Condition != types.ProcessConditionStarted { + condition := p.Processes["t2"].DependsOn["p0"].Condition + t.Errorf("t2 should depend on p0 and it should be Started, got %+v", condition) + } + if p.Processes["t2"].DependsOn["p1-0"].Condition != types.ProcessConditionHealthy { + condition := p.Processes["t2"].DependsOn["p1-0"].Condition + t.Errorf("t2 should depend on p1-0 and it should be Healthy, got %+v", condition) + } + + // depends on p1, with an override for p1-1 + if len(p.Processes["t3"].DependsOn) != 2 { + t.Errorf("t3 should depend on exactly 2 processes") + } + if p.Processes["t3"].DependsOn["p1-0"].Condition != types.ProcessConditionStarted { + condition := p.Processes["t3"].DependsOn["p1-0"].Condition + t.Errorf("t3 should depend on p1-0 and it should be Started, got %v", condition) + } + if p.Processes["t3"].DependsOn["p1-1"].Condition != types.ProcessConditionHealthy { + condition := p.Processes["t3"].DependsOn["p1-1"].Condition + t.Errorf("t3's dependency on p1-1 should be Healthy, got %v", condition) + } +} + func Test_renderTemplates(t *testing.T) { procNoWorkingDir := "noWorkingDir" diff --git a/www/docs/launcher.md b/www/docs/launcher.md index ceb0e1e4..06b70951 100644 --- a/www/docs/launcher.md +++ b/www/docs/launcher.md @@ -42,6 +42,57 @@ processes: replicas: 2 ``` +When replicas are present (`processes.process_name.replicas > 1`), other processes can depend on the group (`process-name`) or specific replicas (`process-name-N` where `N` is a value between 0 and the number of `replicas` minus one). + +```yaml +processes: + consumer: + command: "/some/binary" + replicas: 2 + + # Older versions (<= v1.110.0) require the dependencies to be manually expanded + producer: + command: "/some/other/binary" + depends_on: + consumer-0: + condition: process_healthy + consumer-1: + condition: process_healthy +``` + +```yaml +processes: + consumer: + command: "/some/binary" + replicas: 2 + + # Newer versions (> v1.110.0) allow the group name to be used directly + producer: + command: "/some/other/binary" + depends_on: + consumer: + condition: process_healthy +``` + +```yaml +processes: + consumer: + command: "/some/binary" + replicas: 2 + + # Also we can set conditions for each instance in the group independently + producer: + command: "/some/other/binary" + depends_on: + # set the defaults for the group + consumer: + condition: process_healthy + + # override a single instance + consumer-0: + condition: process_started +``` + To scale a process on the fly CLI: ```shell @@ -101,7 +152,7 @@ processes: echo 'Preparing...' sleep 1 echo 'I am ready to accept connections now' - ready_log_line: "ready to accept connections" # equal to *.ready to accept connections.*\n regex + ready_log_line: "ready to accept connections" # equal to *.ready to accept connections.*\n regex ``` > :bulb: `ready_log_line` and readiness probe are incompatible and can't be used at the same time.