defmodule Object.CoordinationService do @moduledoc """ Distributed coordination service for multi-object operations. Uses consensus algorithms and conflict resolution for large-scale coordination. """ use GenServer require Logger alias Object.MessageRouter defstruct [ :node_id, :coordination_sessions, :conflict_resolution_queue, :consensus_state, :performance_metrics, :worker_pool ] # Client API @doc """ Starts the coordination service GenServer. ## Returns `{:ok, pid}` on successful startup """ def start_link(_) do GenServer.start_link(__MODULE__, :ok, name: __MODULE__) end @doc """ Coordinates a task across multiple objects using consensus algorithms. ## Parameters - `object_ids`: List of object IDs to coordinate - `coordination_task`: Task specification with type, objectives, and constraints ## Returns `{:ok, session_id}` with the coordination session identifier ## Examples iex> Object.CoordinationService.coordinate_objects(["obj1", "obj2"], %{type: :optimization}) {:ok, "session_abc123"} """ def coordinate_objects(object_ids, coordination_task) do GenServer.call(__MODULE__, {:coordinate_objects, object_ids, coordination_task}, 30_000) end @doc """ Resolves conflicts between objects using configured resolution strategies. ## Parameters - `conflict_context`: Context describing the conflict and involved parties ## Returns Resolution result with method and outcome """ def resolve_conflict(conflict_context) do GenServer.call(__MODULE__, {:resolve_conflict, conflict_context}, 15_000) end @doc """ Gets the current status of a coordination session. ## Parameters - `session_id`: ID of the coordination session ## Returns `{:ok, status}` where status is `:voting`, `:in_progress`, `:completed`, or `:failed` """ def get_coordination_status(session_id) do GenServer.call(__MODULE__, {:get_status, session_id}) end @doc """ Adds an object to an existing coordination session. ## Parameters - `session_id`: ID of the coordination session - `object_id`: ID of the object to add ## Returns `:ok` on success, `{:error, reason}` on failure """ def join_coordination(session_id, object_id) do GenServer.call(__MODULE__, {:join_coordination, session_id, object_id}) end @doc """ Removes an object from a coordination session. ## Parameters - `session_id`: ID of the coordination session - `object_id`: ID of the object to remove ## Returns `:ok` on success, `{:ok, :session_cancelled}` if last participant left """ def leave_coordination(session_id, object_id) do GenServer.call(__MODULE__, {:leave_coordination, session_id, object_id}) end @doc """ Gets performance metrics for the coordination service. ## Returns Map with metrics including active sessions, completed sessions, and uptime """ def get_metrics() do GenServer.call(__MODULE__, :get_metrics) end # Server callbacks @impl true def init(:ok) do node_id = Node.self() # Initialize worker pool for coordination tasks pool_size = System.schedulers_online() worker_pool = for _i <- 1..pool_size, do: spawn_link(fn -> coordination_worker() end) state = %__MODULE__{ node_id: node_id, coordination_sessions: %{}, conflict_resolution_queue: :queue.new(), consensus_state: %{}, performance_metrics: init_metrics(), worker_pool: worker_pool } # Start periodic cleanup schedule_cleanup() Logger.info("Coordination Service started on node #{node_id} with #{pool_size} workers") {:ok, state} end @impl true def handle_call({:coordinate_objects, object_ids, coordination_task}, from, state) do session_id = generate_session_id() coordination_session = %{ id: session_id, participants: object_ids, task: coordination_task, status: :initializing, coordinator: from, started_at: DateTime.utc_now(), phases: init_coordination_phases(coordination_task), current_phase: 0, results: %{} } # Use worker pool instead of spawning new process assign_to_worker_pool(state.worker_pool, {:coordinate, coordination_session}) updated_sessions = Map.put(state.coordination_sessions, session_id, coordination_session) updated_state = %{state | coordination_sessions: updated_sessions} {:reply, {:ok, session_id}, updated_state} end @impl true def handle_call({:resolve_conflict, conflict_context}, _from, state) do conflict_id = generate_conflict_id() resolution_task = %{ id: conflict_id, context: conflict_context, strategy: determine_resolution_strategy(conflict_context), created_at: DateTime.utc_now() } # Add to conflict resolution queue updated_queue = :queue.in(resolution_task, state.conflict_resolution_queue) # Process conflict immediately if queue was empty resolution_result = if :queue.len(state.conflict_resolution_queue) == 0 do process_conflict_resolution(resolution_task) else {:queued, conflict_id} end updated_state = %{state | conflict_resolution_queue: updated_queue} {:reply, resolution_result, updated_state} end @impl true def handle_call({:get_status, session_id}, _from, state) do case Map.get(state.coordination_sessions, session_id) do nil -> {:reply, {:error, :session_not_found}, state} session -> {:reply, {:ok, session.status}, state} end end @impl true def handle_call({:join_coordination, session_id, object_id}, _from, state) do case Map.get(state.coordination_sessions, session_id) do nil -> {:reply, {:error, :session_not_found}, state} session -> if object_id in session.participants do {:reply, {:error, :already_participant}, state} else updated_participants = [object_id | session.participants] updated_session = %{session | participants: updated_participants} updated_sessions = Map.put(state.coordination_sessions, session_id, updated_session) {:reply, :ok, %{state | coordination_sessions: updated_sessions}} end end end @impl true def handle_call({:leave_coordination, session_id, object_id}, _from, state) do case Map.get(state.coordination_sessions, session_id) do nil -> {:reply, {:error, :session_not_found}, state} session -> updated_participants = List.delete(session.participants, object_id) if length(updated_participants) == 0 do # Cancel session if no participants left updated_sessions = Map.delete(state.coordination_sessions, session_id) {:reply, {:ok, :session_cancelled}, %{state | coordination_sessions: updated_sessions}} else updated_session = %{session | participants: updated_participants} updated_sessions = Map.put(state.coordination_sessions, session_id, updated_session) {:reply, :ok, %{state | coordination_sessions: updated_sessions}} end end end @impl true def handle_call(:get_metrics, _from, state) do current_metrics = calculate_current_metrics(state) {:reply, current_metrics, state} end @impl true def handle_cast({:coordination_phase_complete, session_id, phase_result}, state) do case Map.get(state.coordination_sessions, session_id) do nil -> Logger.warning("Received phase completion for unknown session: #{session_id}") {:noreply, state} session -> updated_session = advance_coordination_phase(session, phase_result) updated_sessions = Map.put(state.coordination_sessions, session_id, updated_session) # Check if coordination is complete if updated_session.status == :completed do GenServer.reply(updated_session.coordinator, {:ok, updated_session.results}) end {:noreply, %{state | coordination_sessions: updated_sessions}} end end @impl true def handle_info(:cleanup, state) do # Clean up old sessions and process conflict queue current_time = DateTime.utc_now() # Remove sessions older than 1 hour cleaned_sessions = state.coordination_sessions |> Enum.reject(fn {_id, session} -> DateTime.diff(current_time, session.started_at, :second) > 3600 end) |> Enum.into(%{}) # Process next conflict in queue {updated_queue, _processed} = process_conflict_queue(state.conflict_resolution_queue) updated_state = %{state | coordination_sessions: cleaned_sessions, conflict_resolution_queue: updated_queue } schedule_cleanup() {:noreply, updated_state} end # Private functions defp generate_session_id do :crypto.strong_rand_bytes(8) |> Base.encode16() |> String.downcase() end defp generate_conflict_id do :crypto.strong_rand_bytes(6) |> Base.encode16() |> String.downcase() end defp init_coordination_phases(coordination_task) do case coordination_task.type do :system_optimization -> [:analysis, :planning, :execution, :validation] :resource_allocation -> [:discovery, :negotiation, :allocation, :monitoring] :task_distribution -> [:decomposition, :assignment, :execution, :aggregation] _ -> [:preparation, :execution, :completion] end end defp execute_coordination(session) do Logger.info("Starting coordination session #{session.id} with #{length(session.participants)} participants") try do # Execute each phase sequentially final_session = Enum.reduce(session.phases, session, fn phase, acc_session -> execute_coordination_phase(acc_session, phase) end) # Mark as completed _completed_session = %{final_session | status: :completed} GenServer.cast(__MODULE__, {:coordination_phase_complete, session.id, :final}) rescue error -> Logger.error("Coordination session #{session.id} failed: #{inspect(error)}") _failed_session = %{session | status: :failed, results: %{error: error}} GenServer.reply(session.coordinator, {:error, error}) end end defp execute_coordination_phase(session, phase) do Logger.debug("Executing phase #{phase} for session #{session.id}") # Send coordination message to all participants coordination_message = %{ session_id: session.id, phase: phase, task: session.task, participants: session.participants } # Collect responses from all participants phase_results = collect_phase_responses(session.participants, coordination_message) # Update session with phase results updated_results = Map.put(session.results, phase, phase_results) %{session | results: updated_results, current_phase: session.current_phase + 1 } end defp collect_phase_responses(participants, coordination_message) do # Send messages to all participants Enum.each(participants, fn object_id -> message = %{ id: generate_session_id(), from: "coordination_service", to: object_id, type: :coordination, content: coordination_message, timestamp: DateTime.utc_now(), priority: :high, requires_ack: true, ttl: 300 # 5 minutes } MessageRouter.route_message(message) end) # Collect responses (simplified - in real implementation would use proper synchronization) %{ phase_completed: true, participant_responses: length(participants), timestamp: DateTime.utc_now() } end defp advance_coordination_phase(session, _phase_result) do if session.current_phase >= length(session.phases) do %{session | status: :completed} else %{session | status: :in_progress} end end defp determine_resolution_strategy(conflict_context) do case conflict_context.priority do :critical -> :immediate_arbitration :high -> :fast_negotiation _ -> :consensus_building end end defp process_conflict_resolution(resolution_task) do Logger.info("Processing conflict resolution: #{resolution_task.id}") case resolution_task.strategy do :immediate_arbitration -> %{ resolution: :arbitrated, decision: :priority_override, timestamp: DateTime.utc_now() } :fast_negotiation -> %{ resolution: :negotiated, decision: :compromise, timestamp: DateTime.utc_now() } :consensus_building -> %{ resolution: :consensus, decision: :majority_vote, timestamp: DateTime.utc_now() } end end defp process_conflict_queue(queue) do case :queue.out(queue) do {{:value, conflict_task}, new_queue} -> _result = process_conflict_resolution(conflict_task) {new_queue, 1} {:empty, queue} -> {queue, 0} end end defp schedule_cleanup do Process.send_after(self(), :cleanup, 60_000) # 1 minute end defp init_metrics do %{ sessions_created: 0, sessions_completed: 0, sessions_failed: 0, conflicts_resolved: 0, average_coordination_time: 0.0, started_at: DateTime.utc_now() } end defp calculate_current_metrics(state) do active_sessions = map_size(state.coordination_sessions) pending_conflicts = :queue.len(state.conflict_resolution_queue) uptime = DateTime.diff(DateTime.utc_now(), state.performance_metrics.started_at, :second) Map.merge(state.performance_metrics, %{ active_sessions: active_sessions, pending_conflicts: pending_conflicts, uptime_seconds: uptime }) end defp assign_to_worker_pool(worker_pool, task) do # Simple round-robin assignment to available worker worker = Enum.random(worker_pool) send(worker, task) end defp coordination_worker do receive do {:coordinate, session} -> execute_coordination(session) coordination_worker() :stop -> :ok _ -> coordination_worker() end end end