Deployment and lifecycle management for Nix
0
fork

Configure Feed

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

refactor: extract receive_deployment into Garden.Socket.State

SOW-74

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

+87 -258
+24 -24
apps/garden/lib/garden/socket.ex
··· 167 167 ) 168 168 when topic == garden_sid do 169 169 case SowerClient.Orchestration.Deployment.cast(payload) do 170 - {:ok, %{skipped: true} = deployment} -> 171 - Logger.info( 172 - msg: "Deployment skipped by server", 173 - request_id: deployment.request_id, 174 - deployment_sid: deployment.sid 175 - ) 170 + {:ok, deployment} -> 171 + case State.receive_deployment(deployment, socket.active_deployments) do 172 + {:enqueue, active_deployments} -> 173 + Logger.debug( 174 + msg: "Received deployment", 175 + request_id: deployment.request_id, 176 + deployment_sid: deployment.sid 177 + ) 176 178 177 - {:ok, socket} 179 + send(self(), {:run_deployment, deployment.sid}) 180 + {:ok, %{socket | active_deployments: active_deployments}} 178 181 179 - {:ok, deployment} -> 180 - if Map.has_key?(socket.active_deployments, deployment.sid) do 181 - Logger.debug( 182 - msg: "Ignoring duplicate deployment event", 183 - request_id: deployment.request_id, 184 - deployment_sid: deployment.sid 185 - ) 182 + :duplicate -> 183 + Logger.debug( 184 + msg: "Ignoring duplicate deployment event", 185 + request_id: deployment.request_id, 186 + deployment_sid: deployment.sid 187 + ) 186 188 187 - {:ok, socket} 188 - else 189 - Logger.debug( 190 - msg: "Received deployment", 191 - request_id: deployment.request_id, 192 - deployment_sid: deployment.sid 193 - ) 189 + {:ok, socket} 194 190 195 - socket = put_in(socket.active_deployments[deployment.sid], deployment) 196 - send(self(), {:run_deployment, deployment.sid}) 191 + :skipped -> 192 + Logger.info( 193 + msg: "Deployment skipped by server", 194 + request_id: deployment.request_id, 195 + deployment_sid: deployment.sid 196 + ) 197 197 198 - {:ok, socket} 198 + {:ok, socket} 199 199 end 200 200 201 201 {:error, error} ->
+13
apps/garden/lib/garden/socket/state.ex
··· 7 7 that call these functions and execute the returned effects. 8 8 """ 9 9 10 + alias SowerClient.Orchestration.Deployment 10 11 alias SowerClient.Orchestration.DeploymentRequest 11 12 alias SowerClient.Orchestration.Subscription 12 13 ··· 54 55 55 56 def poll_on_connect_subscriptions(subscriptions) do 56 57 Enum.filter(subscriptions, & &1.poll_on_connect) 58 + end 59 + 60 + def receive_deployment(%Deployment{skipped: true}, _active_deployments) do 61 + :skipped 62 + end 63 + 64 + def receive_deployment(%Deployment{} = deployment, active_deployments) do 65 + if Map.has_key?(active_deployments, deployment.sid) do 66 + :duplicate 67 + else 68 + {:enqueue, Map.put(active_deployments, deployment.sid, deployment)} 69 + end 57 70 end 58 71 end
-234
apps/garden/test/garden/client_deployment_test.exs
··· 1 - defmodule Garden.SocketDeploymentTest do 2 - @moduledoc """ 3 - Tests for deployment message handling and duplicate suppression. 4 - """ 5 - use ExUnit.Case, async: false 6 - 7 - alias Garden.Socket 8 - 9 - # Mock socket for testing handle_message callbacks 10 - defmodule MockSocket do 11 - defstruct [:assigns, :active_deployments] 12 - 13 - def new(garden_sid \\ "test_garden_123") do 14 - %__MODULE__{ 15 - assigns: %{garden_sid: garden_sid}, 16 - active_deployments: %{} 17 - } 18 - end 19 - 20 - def with_active_deployment(socket, deployment) do 21 - put_in(socket.active_deployments[deployment.sid], deployment) 22 - end 23 - end 24 - 25 - describe "handle_message for deployment events" do 26 - test "enqueues deployment for new deployment_sid" do 27 - socket = MockSocket.new() 28 - 29 - deployment = %SowerClient.Orchestration.Deployment{ 30 - sid: "deploy_123", 31 - request_id: "dr_456", 32 - seed_deployments: [], 33 - skipped: false 34 - } 35 - 36 - payload = Map.from_struct(deployment) 37 - 38 - {:ok, updated_socket} = 39 - Socket.handle_message( 40 - "garden:test_garden_123", 41 - "deployment", 42 - payload, 43 - socket 44 - ) 45 - 46 - # Verify deployment was added to active_deployments 47 - assert Map.has_key?(updated_socket.active_deployments, "deploy_123") 48 - assert updated_socket.active_deployments["deploy_123"].sid == "deploy_123" 49 - end 50 - 51 - test "ignores duplicate deployment events for already active deployment" do 52 - deployment = %SowerClient.Orchestration.Deployment{ 53 - sid: "deploy_123", 54 - request_id: "dr_456", 55 - seed_deployments: [], 56 - skipped: false 57 - } 58 - 59 - socket = MockSocket.new() |> MockSocket.with_active_deployment(deployment) 60 - 61 - payload = Map.from_struct(deployment) 62 - 63 - {:ok, updated_socket} = 64 - Socket.handle_message( 65 - "garden:test_garden_123", 66 - "deployment", 67 - payload, 68 - socket 69 - ) 70 - 71 - # Verify active_deployments hasn't changed (no duplicate added) 72 - assert map_size(updated_socket.active_deployments) == 1 73 - assert Map.has_key?(updated_socket.active_deployments, "deploy_123") 74 - # Verify the deployment wasn't replaced (same reference would mean same object) 75 - assert updated_socket.active_deployments["deploy_123"] == deployment 76 - end 77 - 78 - test "allows simultaneous deployments for different sids" do 79 - socket = MockSocket.new() 80 - 81 - deployment1 = %SowerClient.Orchestration.Deployment{ 82 - sid: "deploy_123", 83 - request_id: "dr_456", 84 - seed_deployments: [], 85 - skipped: false 86 - } 87 - 88 - deployment2 = %SowerClient.Orchestration.Deployment{ 89 - sid: "deploy_789", 90 - request_id: "dr_abc", 91 - seed_deployments: [], 92 - skipped: false 93 - } 94 - 95 - payload1 = Map.from_struct(deployment1) 96 - payload2 = Map.from_struct(deployment2) 97 - 98 - {:ok, socket} = 99 - Socket.handle_message( 100 - "garden:test_garden_123", 101 - "deployment", 102 - payload1, 103 - socket 104 - ) 105 - 106 - {:ok, socket} = 107 - Socket.handle_message( 108 - "garden:test_garden_123", 109 - "deployment", 110 - payload2, 111 - socket 112 - ) 113 - 114 - # Both deployments should be tracked 115 - assert map_size(socket.active_deployments) == 2 116 - assert Map.has_key?(socket.active_deployments, "deploy_123") 117 - assert Map.has_key?(socket.active_deployments, "deploy_789") 118 - end 119 - 120 - @tag :capture_log 121 - test "handles deployment:error event" do 122 - socket = MockSocket.new() 123 - 124 - payload = %{ 125 - "request_id" => "dr_error_123", 126 - "reason" => "seeds_not_found" 127 - } 128 - 129 - {:ok, updated_socket} = 130 - Socket.handle_message( 131 - "garden:test_garden_123", 132 - "deployment:error", 133 - payload, 134 - socket 135 - ) 136 - 137 - # Socket should remain unchanged on error 138 - assert updated_socket.active_deployments == %{} 139 - end 140 - 141 - test "handles skipped deployment" do 142 - socket = MockSocket.new() 143 - 144 - deployment = %SowerClient.Orchestration.Deployment{ 145 - sid: "deploy_existing", 146 - request_id: "dr_skip_456", 147 - seed_deployments: [], 148 - skipped: true 149 - } 150 - 151 - payload = Map.from_struct(deployment) 152 - 153 - {:ok, updated_socket} = 154 - Socket.handle_message( 155 - "garden:test_garden_123", 156 - "deployment", 157 - payload, 158 - socket 159 - ) 160 - 161 - # Skipped deployments should not be added to active_deployments 162 - assert updated_socket.active_deployments == %{} 163 - end 164 - 165 - test "handles invalid deployment payload gracefully" do 166 - socket = MockSocket.new() 167 - 168 - payload = %{ 169 - "invalid" => "data" 170 - } 171 - 172 - {:ok, _socket} = 173 - Socket.handle_message( 174 - "garden:test_garden_123", 175 - "deployment", 176 - payload, 177 - socket 178 - ) 179 - 180 - # Should return {:ok, socket} even on error, not crash 181 - # The error is logged but not raised 182 - end 183 - 184 - test "duplicate suppression maintains separate state per deployment_sid" do 185 - socket = MockSocket.new() 186 - 187 - # First deployment - should be accepted 188 - deployment1 = %SowerClient.Orchestration.Deployment{ 189 - sid: "deploy_first", 190 - request_id: "dr_1", 191 - seed_deployments: [], 192 - skipped: false 193 - } 194 - 195 - {:ok, socket} = 196 - Socket.handle_message( 197 - "garden:test_garden_123", 198 - "deployment", 199 - Map.from_struct(deployment1), 200 - socket 201 - ) 202 - 203 - # Try to add duplicate of first - should be ignored 204 - {:ok, socket} = 205 - Socket.handle_message( 206 - "garden:test_garden_123", 207 - "deployment", 208 - Map.from_struct(deployment1), 209 - socket 210 - ) 211 - 212 - # Second deployment (different sid) - should be accepted 213 - deployment2 = %SowerClient.Orchestration.Deployment{ 214 - sid: "deploy_second", 215 - request_id: "dr_2", 216 - seed_deployments: [], 217 - skipped: false 218 - } 219 - 220 - {:ok, socket} = 221 - Socket.handle_message( 222 - "garden:test_garden_123", 223 - "deployment", 224 - Map.from_struct(deployment2), 225 - socket 226 - ) 227 - 228 - # Should have both deployments, but not the duplicate 229 - assert map_size(socket.active_deployments) == 2 230 - assert Map.has_key?(socket.active_deployments, "deploy_first") 231 - assert Map.has_key?(socket.active_deployments, "deploy_second") 232 - end 233 - end 234 - end
+50
apps/garden/test/garden/socket/state_test.exs
··· 3 3 4 4 alias Garden.Socket.State 5 5 alias SowerClient.Orchestration.DeploymentRequest 6 + alias SowerClient.Orchestration.Deployment 6 7 alias SowerClient.Orchestration.GardenSeedsReport 7 8 alias SowerClient.Orchestration.Subscription 8 9 ··· 123 124 ] 124 125 125 126 assert State.poll_on_connect_subscriptions(subs) == [] 127 + end 128 + end 129 + 130 + describe "receive_deployment/2" do 131 + test "enqueues new deployment" do 132 + deployment = %Deployment{ 133 + sid: "deploy_123", 134 + request_id: "dr_456", 135 + seed_deployments: [], 136 + skipped: false 137 + } 138 + 139 + assert {:enqueue, active} = State.receive_deployment(deployment, %{}) 140 + assert Map.has_key?(active, "deploy_123") 141 + assert active["deploy_123"].sid == "deploy_123" 142 + end 143 + 144 + test "returns duplicate for already active deployment" do 145 + deployment = %Deployment{ 146 + sid: "deploy_123", 147 + request_id: "dr_456", 148 + seed_deployments: [], 149 + skipped: false 150 + } 151 + 152 + active = %{"deploy_123" => deployment} 153 + 154 + assert :duplicate = State.receive_deployment(deployment, active) 155 + end 156 + 157 + test "returns skipped for skipped deployment" do 158 + deployment = %Deployment{ 159 + sid: "deploy_123", 160 + request_id: "dr_456", 161 + seed_deployments: [], 162 + skipped: true 163 + } 164 + 165 + assert :skipped = State.receive_deployment(deployment, %{}) 166 + end 167 + 168 + test "allows simultaneous deployments for different sids" do 169 + d1 = %Deployment{sid: "deploy_1", request_id: "dr_1", seed_deployments: [], skipped: false} 170 + d2 = %Deployment{sid: "deploy_2", request_id: "dr_2", seed_deployments: [], skipped: false} 171 + 172 + {:enqueue, active} = State.receive_deployment(d1, %{}) 173 + {:enqueue, active} = State.receive_deployment(d2, active) 174 + 175 + assert map_size(active) == 2 126 176 end 127 177 end 128 178 end