Deployment and lifecycle management for Nix
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: rename Garden.Socket.State to Garden.Socket.Lifecycle

SOW-74

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+42 -41
+10 -10
apps/garden/lib/garden/socket.ex
··· 4 4 require Logger 5 5 6 6 alias Garden.Scheduler 7 - alias Garden.Socket.State 7 + alias Garden.Socket.Lifecycle 8 8 alias Garden.Storage 9 9 alias SowerClient.Orchestration.DeploymentStatus 10 10 alias SowerClient.Orchestration.SeedDeploymentStatus ··· 16 16 17 17 @impl Slipstream 18 18 def handle_cast({:deployment_request, %{sid: sid}, force?}, socket) do 19 - {:ok, upgrade_request} = State.build_deployment_request(sid, force?) 19 + {:ok, upgrade_request} = Lifecycle.build_deployment_request(sid, force?) 20 20 {:ok, _ref} = push_message(socket, upgrade_request) 21 21 22 22 {:noreply, socket} ··· 26 26 def handle_cast(:report_seeds, socket) do 27 27 subscriptions = Map.get(Storage.read(), :subscriptions, []) 28 28 29 - case State.build_seed_report(subscriptions) do 29 + case Lifecycle.build_seed_report(subscriptions) do 30 30 :no_profiles -> 31 31 Logger.debug( 32 32 msg: "No profiles found for any targets", ··· 63 63 subscriptions = 64 64 case await_reply(ref) do 65 65 {:ok, %{"subscriptions" => registered}} -> 66 - State.merge_subscriptions(config_subscriptions, registered) 66 + Lifecycle.merge_subscriptions(config_subscriptions, registered) 67 67 68 68 {:error, error} -> 69 69 Logger.error(msg: "Failed to sync subscriptions", error: error) ··· 73 73 Scheduler.refresh_subscriptions(subscriptions) 74 74 Garden.Storage.put(:subscriptions, subscriptions) 75 75 76 - State.poll_on_connect_subscriptions(subscriptions) 76 + Lifecycle.poll_on_connect_subscriptions(subscriptions) 77 77 |> Enum.each(fn sub -> 78 78 Task.Supervisor.start_child(Garden.TaskSupervisor, fn -> 79 79 deploy(sub) ··· 168 168 when topic == garden_sid do 169 169 case SowerClient.Orchestration.Deployment.cast(payload) do 170 170 {:ok, deployment} -> 171 - case State.receive_deployment(deployment, socket.active_deployments) do 171 + case Lifecycle.receive_deployment(deployment, socket.active_deployments) do 172 172 {:enqueue, active_deployments} -> 173 173 Logger.debug( 174 174 msg: "Received deployment", ··· 236 236 storage = Garden.Storage.read() 237 237 238 238 {:join, garden_sid, persist?: persist?} = 239 - State.process_hello_reply(garden_sid, storage.garden_sid) 239 + Lifecycle.process_hello_reply(garden_sid, storage.garden_sid) 240 240 241 241 if persist? do 242 242 storage |> Map.put(:garden_sid, garden_sid) |> Garden.Storage.write() ··· 264 264 265 265 @impl Slipstream 266 266 def handle_info({:run_deployment, sid}, socket) do 267 - case State.lookup_deployment(sid, socket.active_deployments) do 267 + case Lifecycle.lookup_deployment(sid, socket.active_deployments) do 268 268 :not_found -> 269 269 Logger.warning(msg: "Deployment not found", sid: sid) 270 270 {:noreply, socket} ··· 312 312 end 313 313 314 314 def handle_info({:deployment_completed, sid, result}, socket) do 315 - case State.complete_deployment(sid, result, socket.active_deployments) do 315 + case Lifecycle.complete_deployment(sid, result, socket.active_deployments) do 316 316 :not_found -> 317 317 Logger.warning(msg: "Deployment not found during completion", sid: sid) 318 318 {:noreply, socket} ··· 328 328 end 329 329 330 330 def handle_info(:check_pending_reload, socket) do 331 - if State.should_reload?(socket.active_deployments, Garden.take_pending_reload()) do 331 + if Lifecycle.should_reload?(socket.active_deployments, Garden.take_pending_reload()) do 332 332 reload_garden_service() 333 333 end 334 334
+2 -2
apps/garden/lib/garden/socket/state.ex apps/garden/lib/garden/socket/lifecycle.ex
··· 1 - defmodule Garden.Socket.State do 1 + defmodule Garden.Socket.Lifecycle do 2 2 @moduledoc """ 3 - Pure state transition functions for Garden.Socket. 3 + Pure lifecycle transition functions for Garden.Socket. 4 4 5 5 Each function takes relevant state and returns a result without 6 6 performing side effects. The socket callbacks are thin wrappers
+30 -29
apps/garden/test/garden/socket/state_test.exs apps/garden/test/garden/socket/lifecycle_test.exs
··· 1 - defmodule Garden.Socket.StateTest do 1 + defmodule Garden.Socket.LifecycleTest do 2 2 use ExUnit.Case, async: true 3 3 4 - alias Garden.Socket.State 4 + alias Garden.Socket.Lifecycle 5 + 5 6 alias SowerClient.Orchestration.DeploymentRequest 6 7 alias SowerClient.Orchestration.Deployment 7 8 alias SowerClient.Orchestration.GardenSeedsReport ··· 9 10 10 11 describe "build_deployment_request/2" do 11 12 test "builds request with subscription sid" do 12 - {:ok, %DeploymentRequest{} = request} = State.build_deployment_request("sub_123", false) 13 + {:ok, %DeploymentRequest{} = request} = Lifecycle.build_deployment_request("sub_123", false) 13 14 14 15 assert request.subscription_sids == ["sub_123"] 15 16 assert request.request_id != nil 16 17 end 17 18 18 19 test "sets force flag when true" do 19 - {:ok, %DeploymentRequest{} = request} = State.build_deployment_request("sub_123", true) 20 + {:ok, %DeploymentRequest{} = request} = Lifecycle.build_deployment_request("sub_123", true) 20 21 21 22 assert request.force == true 22 23 end 23 24 24 25 test "omits force flag when false" do 25 - {:ok, %DeploymentRequest{} = request} = State.build_deployment_request("sub_123", false) 26 + {:ok, %DeploymentRequest{} = request} = Lifecycle.build_deployment_request("sub_123", false) 26 27 27 28 assert request.force == false 28 29 end ··· 38 39 }) 39 40 40 41 assert {:report, ^report} = 41 - State.build_seed_report(subscriptions, fn _subs -> report end) 42 + Lifecycle.build_seed_report(subscriptions, fn _subs -> report end) 42 43 end 43 44 44 45 test "returns no_profiles when subscriptions exist but no profiles found" do 45 46 subscriptions = [%{seed_type: "nixos", seed_name: "host", rules: []}] 46 47 report = GardenSeedsReport.cast!(%{profiles: []}) 47 48 48 - assert :no_profiles = State.build_seed_report(subscriptions, fn _subs -> report end) 49 + assert :no_profiles = Lifecycle.build_seed_report(subscriptions, fn _subs -> report end) 49 50 end 50 51 51 52 test "returns report when subscriptions are empty" do 52 53 report = GardenSeedsReport.cast!(%{profiles: []}) 53 54 54 - assert {:report, ^report} = State.build_seed_report([], fn _subs -> report end) 55 + assert {:report, ^report} = Lifecycle.build_seed_report([], fn _subs -> report end) 55 56 end 56 57 end 57 58 ··· 67 68 %{"seed_name" => "user", "seed_type" => "home-manager", "sid" => "sub_def"} 68 69 ] 69 70 70 - result = State.merge_subscriptions(config_subs, registered) 71 + result = Lifecycle.merge_subscriptions(config_subs, registered) 71 72 72 73 assert length(result) == 2 73 74 assert Enum.find(result, &(&1.seed_name == "host")).sid == "sub_abc" ··· 85 86 %{"seed_name" => "host", "seed_type" => "nixos", "sid" => "sub_abc"} 86 87 ] 87 88 88 - result = State.merge_subscriptions(config_subs, registered) 89 + result = Lifecycle.merge_subscriptions(config_subs, registered) 89 90 90 91 assert length(result) == 1 91 92 assert hd(result).seed_name == "host" ··· 96 97 Subscription.cast!(%{seed_name: "host", seed_type: "nixos"}) 97 98 ] 98 99 99 - assert State.merge_subscriptions(config_subs, []) == [] 100 + assert Lifecycle.merge_subscriptions(config_subs, []) == [] 100 101 end 101 102 end 102 103 ··· 112 113 Subscription.cast!(%{seed_name: "user", seed_type: "home-manager", sid: "sub_2"}) 113 114 ] 114 115 115 - result = State.poll_on_connect_subscriptions(subs) 116 + result = Lifecycle.poll_on_connect_subscriptions(subs) 116 117 117 118 assert length(result) == 1 118 119 assert hd(result).seed_name == "host" ··· 123 124 Subscription.cast!(%{seed_name: "host", seed_type: "nixos", sid: "sub_1"}) 124 125 ] 125 126 126 - assert State.poll_on_connect_subscriptions(subs) == [] 127 + assert Lifecycle.poll_on_connect_subscriptions(subs) == [] 127 128 end 128 129 end 129 130 ··· 136 137 skipped: false 137 138 } 138 139 139 - assert {:enqueue, active} = State.receive_deployment(deployment, %{}) 140 + assert {:enqueue, active} = Lifecycle.receive_deployment(deployment, %{}) 140 141 assert Map.has_key?(active, "deploy_123") 141 142 assert active["deploy_123"].sid == "deploy_123" 142 143 end ··· 151 152 152 153 active = %{"deploy_123" => deployment} 153 154 154 - assert :duplicate = State.receive_deployment(deployment, active) 155 + assert :duplicate = Lifecycle.receive_deployment(deployment, active) 155 156 end 156 157 157 158 test "returns skipped for skipped deployment" do ··· 162 163 skipped: true 163 164 } 164 165 165 - assert :skipped = State.receive_deployment(deployment, %{}) 166 + assert :skipped = Lifecycle.receive_deployment(deployment, %{}) 166 167 end 167 168 168 169 test "allows simultaneous deployments for different sids" do 169 170 d1 = %Deployment{sid: "deploy_1", request_id: "dr_1", seed_deployments: [], skipped: false} 170 171 d2 = %Deployment{sid: "deploy_2", request_id: "dr_2", seed_deployments: [], skipped: false} 171 172 172 - {:enqueue, active} = State.receive_deployment(d1, %{}) 173 - {:enqueue, active} = State.receive_deployment(d2, active) 173 + {:enqueue, active} = Lifecycle.receive_deployment(d1, %{}) 174 + {:enqueue, active} = Lifecycle.receive_deployment(d2, active) 174 175 175 176 assert map_size(active) == 2 176 177 end ··· 187 188 188 189 active = %{"deploy_123" => deployment} 189 190 190 - assert {:ok, ^deployment} = State.lookup_deployment("deploy_123", active) 191 + assert {:ok, ^deployment} = Lifecycle.lookup_deployment("deploy_123", active) 191 192 end 192 193 193 194 test "returns not_found when missing" do 194 - assert :not_found = State.lookup_deployment("deploy_123", %{}) 195 + assert :not_found = Lifecycle.lookup_deployment("deploy_123", %{}) 195 196 end 196 197 end 197 198 ··· 207 208 active = %{"deploy_123" => deployment} 208 209 209 210 assert {:ok, result, updated_active} = 210 - State.complete_deployment("deploy_123", :success, active) 211 + Lifecycle.complete_deployment("deploy_123", :success, active) 211 212 212 213 assert result.request_id == "dr_456" 213 214 assert result.deployment_sid == "deploy_123" ··· 217 218 end 218 219 219 220 test "returns not_found when deployment missing" do 220 - assert :not_found = State.complete_deployment("deploy_123", :success, %{}) 221 + assert :not_found = Lifecycle.complete_deployment("deploy_123", :success, %{}) 221 222 end 222 223 223 224 test "preserves other deployments in map" do ··· 226 227 227 228 active = %{"deploy_1" => d1, "deploy_2" => d2} 228 229 229 - {:ok, _result, updated_active} = State.complete_deployment("deploy_1", :success, active) 230 + {:ok, _result, updated_active} = Lifecycle.complete_deployment("deploy_1", :success, active) 230 231 231 232 assert map_size(updated_active) == 1 232 233 assert Map.has_key?(updated_active, "deploy_2") ··· 235 236 236 237 describe "should_reload?/2" do 237 238 test "returns true when no active deployments and pending reload" do 238 - assert State.should_reload?(%{}, true) 239 + assert Lifecycle.should_reload?(%{}, true) 239 240 end 240 241 241 242 test "returns false when active deployments exist" do ··· 248 249 } 249 250 } 250 251 251 - refute State.should_reload?(active, true) 252 + refute Lifecycle.should_reload?(active, true) 252 253 end 253 254 254 255 test "returns false when no pending reload" do 255 - refute State.should_reload?(%{}, false) 256 + refute Lifecycle.should_reload?(%{}, false) 256 257 end 257 258 end 258 259 259 260 describe "process_hello_reply/2" do 260 261 test "returns persist true when garden_sid differs from stored" do 261 262 assert {:join, "garden_new", persist?: true} = 262 - State.process_hello_reply("garden_new", "garden_old") 263 + Lifecycle.process_hello_reply("garden_new", "garden_old") 263 264 end 264 265 265 266 test "returns persist false when garden_sid matches stored" do 266 267 assert {:join, "garden_same", persist?: false} = 267 - State.process_hello_reply("garden_same", "garden_same") 268 + Lifecycle.process_hello_reply("garden_same", "garden_same") 268 269 end 269 270 270 271 test "returns persist true when stored is nil" do 271 272 assert {:join, "garden_new", persist?: true} = 272 - State.process_hello_reply("garden_new", nil) 273 + Lifecycle.process_hello_reply("garden_new", nil) 273 274 end 274 275 end 275 276 end