Hacker News new | past | comments | ask | show | jobs | submit login

With an idempotent API, starting a VM and doing something with it can look like this:

    let new_id = generate_id();
    retry_with_backoff(() => api.make_vm(new_id));
    retry_with_backoff(() => api.do_thing_with_vm(new_id));
This works even if any individual API calls fail, or if the API call makes it to the API server but the response fails to make it to the client.

If the APIs aren't idempotent, then you would have to do this to get the same behavior:

    let new_id = generate_id();
    retry_with_backoff(() => {
      try {
        api.make_vm(new_id);
      } catch (e) {
        if (e.info && e.info.code === 'vm_already_exists') {
          return;
        }
        throw e;
      }
    });
    retry_with_backoff(() => {
      try {
        api.do_thing_with_vm(new_id);
      } catch (e) {
        if (e.info && e.info.code === 'thing_already_done') {
          return;
        }
        throw e;
      }
    });
This nonidempotent API is harder to use. Someone that doesn't know about these error codes or the fact that the API isn't idempotent will write code without the try-catch blocks that doesn't handle retries correctly. With the idempotent API, users fall into the pit of success where things just work without them having to know the details about each of the edge cases.

The nonidempotent API is exposing some extra data to the user, but it's not super useful. You basically always want to treat the vm_already_exists error identically to a success response. Maybe you also want to log some data about how many retries were necessary so you can figure out how spotty the network connection is, but there's no reason that couldn't work with the idempotent API either. The idempotent API could include a header about whether the action was already taken previously.

Consider how TCP connections are used by applications. Your application doesn't have to opt in to handling packets that were resent. The fact that some packets had to be resent is by default just an implementation detail. You have to opt in to get information about the resent packets; by default they're handled like regular successful packets. Idempotent APIs are about making handling retries work by default in a very similar way.




Lets start simple, your example assumes that you generate the id yourself. In my experience a common API usage pattern would look more like

  try:
      vm_id = api.make_vm()
  except SomeError as e:
      log.error(e)
  else:
      res = api.do_thing_with_vm(vm_id)
and in your example, if we are generating ids ourselves, we still have to verify that we got the right VM. If your ids are provably unique, there is no reason to generate them, the API can take care of that, but if you want something like a named entity, you have a problem. What if the name is already taken? So your code would look more like

    new_id = generate_id()
    try:
        vm = api.get_vm(new_id)
    except VM_DoesNotExist:
        vm = api.make_vm(new_id)
    except SomeError as e:
        log.error(e)
    else:
        api.do_thing_with_vm(new_id)
because if the make_vm API simply returns a VM whether it was created or not, it is entirely possible that you are getting a VM that is busy doing something else for some other process.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: