Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
punkpeye opened this issue Dec 10, 2024 · 4 comments
Open

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

punkpeye opened this issue Dec 10, 2024 · 4 comments

Comments

@punkpeye
Copy link

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
Copy link
Author

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
Copy link
Author

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
Copy link
Author

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
Copy link
Author

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();
    }
  }
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant