View Source NoNoncense

Generate locally unique nonces (number-only-used-once) in distributed Elixir.

Nonces come in multiple varians:

  • counter nonces that are unique but predictable and can be generated incredibly quickly
  • sortable nonces (Snowflake IDs) that have an accurate creation timestamp in their first bits
  • encrypted nonces that are unique but unpredictable

Installation

The package is hosted on hex.pm and can be installed by adding :no_noncense to your list of dependencies in mix.exs:

def deps do
  [
    {:no_noncense, "~> 0.0.3"}
  ]
end

Docs

Documentation can be found on hexdocs.pm.

Usage

Note that NoNoncense is not a GenServer. Instead, it stores its initial state using :persistent_term and its internal counter using :atomics. Because :persistent_term triggers a garbage collection cycle on writes, it is recommended to initialize your NoNoncense instance(s) at application start, when there is hardly any garbage to collect.

# lib/my_app/application.ex
# generate a machine ID, start conflict guard and initialize a NoNoncense instance
defmodule MyApp.Application do
  use Application

  def start(_type, _args) do
    # grab your node_list from your application environment
    machine_id = NoNoncense.MachineId.id!(node_list: [:"myapp@127.0.0.1"])
    :ok = NoNoncense.init(machine_id: machine_id)

    children =
      [
        # optional but recommended
        {NoNoncense.MachineId.ConflictGuard, [machine_id: machine_id]}
      ]

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Then you can generate nonces.

# generate counter nonces
iex> <<_::64>> = NoNoncense.nonce(64)
iex> <<_::96>> = NoNoncense.nonce(96)
iex> <<_::128>> = NoNoncense.nonce(128)

# generate sortable nonces
iex> <<_::64>> = NoNoncense.sortable_nonce(64)
iex> <<_::96>> = NoNoncense.sortable_nonce(96)
iex> <<_::128>> = NoNoncense.sortable_nonce(128)

# generate encrypted nonces
# be sure to read the NoNoncense docs before using 64/96 bits encrypted nonces
iex> <<_::64>> = NoNoncense.encrypted_nonce(64, :crypto.strong_rand_bytes(24))
iex> <<_::96>> = NoNoncense.encrypted_nonce(96, :crypto.strong_rand_bytes(24))
iex> <<_::128>> = NoNoncense.encrypted_nonce(128, :crypto.strong_rand_bytes(32))

Benchmarks

On Debian Bookworm, AMD 9700X (8C 16T), 32GB, 990 Pro.

nonce(128) single:             54_981_091 ops/s
nonce(96) single:              44_348_625 ops/s
nonce(128) multi:              37_745_241 ops/s
nonce(64) multi:               37_617_656 ops/s
nonce(96) multi:               37_552_652 ops/s
sortable_nonce(96) multi:      35_124_078 ops/s
sortable_nonce(128) multi:     34_819_544 ops/s
nonce(64) single:              25_847_171 ops/s
sortable_nonce(128) single:    21_776_095 ops/s
sortable_nonce(96) single:     21_016_157 ops/s
encrypted_nonce(128) multi:    13_485_052 ops/s
encrypted_nonce(64) multi:     11_049_795 ops/s
encrypted_nonce(96) multi:     10_438_713 ops/s
sortable_nonce(64) multi:       8_192_779 ops/s
sortable_nonce(64) single:      8_191_835 ops/s
strong_rand_bytes(8) multi:     4_859_998 ops/s
strong_rand_bytes(12) multi:    4_856_265 ops/s
strong_rand_bytes(16) multi:    4_807_786 ops/s
strong_rand_bytes(8) single:    2_731_625 ops/s
strong_rand_bytes(12) single:   2_723_526 ops/s
strong_rand_bytes(16) single:   2_723_058 ops/s
encrypted_nonce(128) single:    2_446_565 ops/s
encrypted_nonce(64) single:     1_209_200 ops/s
encrypted_nonce(96) single:     1_118_734 ops/s

Some things of note:

  • NoNoncense nonces generate much faster than random binaries.
  • All methods are quick enough to handle very high peak loads.
  • The plain (counter) nonce generation rate is hardly influenced by multithreading and seems to hit a bottleneck of some kind, probably to do with :persistent_term or :atomics. Still, it hits a really high rate.
  • Encrypting the nonce exacts a very hefty penalty, but parallellization scales well to alleviate the issue.
  • Triple DES sucks.