docs icon indicating copy to clipboard operation
docs copied to clipboard

Issue with the "Session Affinity (a.k.a. Sticky Sessions)" doc

Open punkpeye opened this issue 11 months ago • 4 comments

I found an issue with this document.

Title: Session Affinity (a.k.a. Sticky Sessions) Location: https://fly.io/docs/blueprints/sticky-sessions/ Source: https://github.com/superfly/docs/blob/main/blueprints/sticky-sessions.html.md

Describe the issue

The whole section about "Fly-Force-Instance_Id" makes little sense.

It talks about some @hotwired/stimulus, which I (as a reader) have no exposure to.

All I want is to drop a cookie on the client's device, and if a request comes in with that ID, it routes to the appropriate server, and if that server cannot be found, then it routes to a random server and sets a new cookie.

How do I do that?

punkpeye avatar Dec 10 '24 05:12 punkpeye

Clients accessing Fly.io apps may set the fly-force-instance-id header to ensure that the request is sent to only a specific Machine.

If the Machine is deleted or not found, no other Machines will be tried.

https://fly.io/docs/networking/dynamic-request-routing/#the-fly-replay-response-header

The entire document has no mention of cookies, so it seems like that's not even possible.

Even if I somehow injected the header, the fact that "If the Machine is deleted or not found, no other Machines will be tried.", makes it useless.

punkpeye avatar Dec 10 '24 05:12 punkpeye

I’ll start by saying that I agree that ideally this should be handled by the platform, and perhaps some day it will be. Meanwhile…

Consider a deployment with two VMs: VM1 and VM2.

A POST request comes in with some data. It gets routed to VM1. That VM writes the data to the filesystem. Before responding it gets FLY_MACHINE_ID from the ENV and adds it as a cookie.

Subsequently a GET request comes in requiring that same data. Unfortunately it gets routed to VM2. VM2 compares the cookie against FLY_MACHINE_ID and sees that it is different. So instead of proceeding, it responds with a Fly-Replay header specifying the desired machine. Fly.io replays the same get request, but this time on VM1, which successfully processes the request.

https://community.fly.io/t/sticky-sessions/19615/4

Not sure if this will work with SSE, but I will give it a try.

punkpeye avatar Dec 10 '24 05:12 punkpeye

Hopefully this helps others. Here is a fastify solution:

if (config.FLY_MACHINE_ID) {
  const { getMachines, stop } = createMachinePoller();

  registerShutdownHandler({
    name: 'fly-machine-poller',
    run: async () => {
      stop();
    },
    weight: 400,
  });

  app.addHook('onRequest', async (request, reply) => {
    const machineCookie = request.cookies['glama-machine-id'];

    if (!machineCookie) {
      reply.setCookie('glama-machine-id', config.FLY_MACHINE_ID, {
        maxAge: getDuration('1 day', 'milliseconds'),
      });

      return;
    }

    if (machineCookie !== config.FLY_MACHINE_ID) {
      const startedMachineIds = getMachines()
        .filter((machine) => machine.state === 'started')
        .map((machine) => machine.id);

      if (startedMachineIds.includes(machineCookie)) {
        reply
          .header('fly-replay', `instance=${machineCookie}`)
          .code(307)
          .send();
      }
    }
  });
}

punkpeye avatar Dec 10 '24 05:12 punkpeye

After several failed attempts, here is something that works.

// https://fly.io/docs/networking/dynamic-request-routing/
app.addHook('onRequest', async (request, reply) => {
  // The current machine is terminating, so we need to find a new machine to replay.
  if (shutdownHandler.getStatus() === 'terminating') {
    const startedMachineIds = getMachines()
      .filter((machine) => machine.state === 'started')
      .filter((machine) => machine.id !== config.FLY_MACHINE_ID)
      .map((machine) => machine.id);

    const machineId = startedMachineIds[0];

    if (machineId) {
      reply
        .header('fly-replay', `instance=${machineId};elsewhere=true`)
        .code(307)
        .send();
    } else {
      reply.code(503).send('Could not find a machine to replay');
    }

    return;
  }

  const machineCookie = request.cookies['glama-machine-id'];

  // We are handling a request if the machine cookie is not set
  // or if request is being replayed from a different machine.
  if (!machineCookie || request.headers['fly-replay-src']) {
    reply.setCookie('glama-machine-id', config.FLY_MACHINE_ID, {
      maxAge: getDuration('1 day', 'milliseconds'),
    });

    return;
  }

  // We are asking another machine to replay the request
  // if the machine cookie does not match the current machine.
  if (machineCookie !== config.FLY_MACHINE_ID) {
    const startedMachineIds = getMachines()
      .filter((machine) => machine.state === 'started')
      .map((machine) => machine.id);

    if (startedMachineIds.includes(machineCookie)) {
      reply
        .header('fly-replay', `instance=${machineCookie}`)
        .code(307)
        .send();
    }
  }
});

punkpeye avatar Dec 10 '24 18:12 punkpeye