Last active
January 22, 2026 02:56
-
-
Save clutterstack/97a66b4b7d7d82babf586962e80ade95 to your computer and use it in GitHub Desktop.
Machine affinity for an Elixir Phoenix application on Fly.io. Near copy of @PJUllrich's solution at https://peterullrich.com/request-routing-and-sticky-sessions-in-phoenix-on-fly. More words at https://clutterstack.com/posts/2025-06-01-liveview-machine-affinity
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Copied from https://peterullrich.com/request-routing-and-sticky-sessions-in-phoenix-on-fly | |
| defmodule UselessMachineWeb.Endpoint do | |
| use Phoenix.Endpoint, otp_app: :useless_machine | |
| import Plug.Conn | |
| def call(conn, opts) do | |
| case UselessMachineWeb.RouteHandler.call(conn, opts) do | |
| %Plug.Conn{halted: true} = conn -> conn # if it was redirected, it gets halted in the plug; leave it halted | |
| conn -> super(conn, opts) # if the plug passed it through, now send it to the default version of call/2 | |
| end | |
| end | |
| # ... | |
| end | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Modification of https://peterullrich.com/request-routing-and-sticky-sessions-in-phoenix-on-fly | |
| defmodule UselessMachineWeb.RouteHandler do | |
| use Plug.Builder # saves writing explicit init and stuff | |
| import Plug.Conn | |
| require Logger | |
| @cookie_key "fly-machine-id" | |
| @cookie_ttl 1 * 60 * 1000 # ms | |
| def call(conn, opts) do | |
| conn | |
| |> fetch_query_params() | |
| |> fetch_cookies() | |
| |> handle_conn(opts) | |
| end | |
| def handle_conn(%Plug.Conn{params: params} = conn, _opts) do | |
| machine_id = System.get_env("FLY_MACHINE_ID") # Ullrich puts it in his application config: Application.get_env(:chat, :fly_machine_id) | |
| param_id = Map.get(params, "instance") | |
| path_base = Path.basename(conn.request_path) # I added this so I can exempt health checks that come from localhost | |
| cookie_id = Map.get(conn.req_cookies, @cookie_key, machine_id) | |
| # Logger.info("In RouteHandler, request path is #{conn.request_path}") | |
| # Logger.info("In RouteHandler, param_id is #{param_id}") | |
| # Logger.info("In RouteHandler, cookie_id is #{cookie_id}") | |
| cond do | |
| path_base && path_base == "health" -> | |
| Logger.info("Health endpoint. Carry on as normal.") | |
| conn | |
| # The initial request will contain the Machine ID in a parameter. Check for this first. | |
| # If the param matches the Machine ID, give the client a cookie and let it in. | |
| param_id && param_id == machine_id -> | |
| Logger.info("Correct machine based on parameter #{param_id}. Set cookie and let pass.") | |
| put_resp_cookie(conn, @cookie_key, machine_id, max_age: @cookie_ttl) | |
| # If there's a param but it's the wrong one, try a fly-replay. | |
| param_id && param_id != machine_id -> | |
| Logger.info("Incorrect machine #{machine_id} based on parameter #{param_id}. Redirect.") | |
| redirect_to_machine(conn, param_id) | |
| # If there wasn't a param to check against, check for a cookie that matches the Machine ID. | |
| # Right cookie; carry on | |
| cookie_id && cookie_id == machine_id -> | |
| Logger.info("Correct machine based on cookie #{cookie_id}. Let pass.") | |
| # Logger.info("(request path is #{conn.request_path})") | |
| conn | |
| # No param, wrong cookie; fly-replay | |
| cookie_id && cookie_id != machine_id -> | |
| Logger.info("Incorrect machine #{machine_id} based on cookie #{cookie_id}. Redirect.") | |
| # Logger.info("(request path is #{conn.request_path})") | |
| redirect_to_machine(conn, cookie_id) | |
| # Unlike Ullrich's use case, I have only one path in my router that visitors should access. | |
| # If the initial request doesn't have the Machine ID param, we shoudn't serve any content. | |
| true -> | |
| Logger.info("No parameter or cookie. Send 404.") | |
| conn | |
| |> send_resp(404, "Not found") | |
| |> halt() | |
| end | |
| end | |
| defp redirect_to_machine(conn, requested_machine) do | |
| conn | |
| |> put_resp_header("fly-replay", "instance=#{requested_machine}") | |
| |> send_resp(307, "") # Skip the Phoenix controller text. Note that fly-proxy doesn't care what status or body you send with a fly-replay header. | |
| |> halt() | |
| end | |
| end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment