CoderRing behaviour (CoderRing v0.2.3) View Source

CoderRing

Provides short, unique codes upon request.

Methodology

The database is seeded with every possible base code. With the default of 4 characters, each being one of 32 possible characters, the database will be populated with 1,048,576 possibilities (without the expletives filter). The 32 possible characters include 2-9 and A-Z, not including I or O. This set was chosen to avoid ambiguity for human eyes.

When a code is requested with get_code/2, "max" is decremented by 1 from the max used in finding the last code. (If it's the first code fetched, "max" is the last record.) "r" is then a random number between 1 and "max", inclusive. Records "r" and "max" are found in the table by their "position" field. (Records positions are numbered from 1). The values for records "r" and "max" are reversed, and the value landing out of bounds for future codes in the "max" position is returned as the next code. This gives off the look of truly random codes, but they never repeat.

Once the cycle completes, "max" is reset to the last record again and it repeats. The records in the table will be randomized, but it doesn't really matter since the records will again be pulled in random order.

What is a Code?

A code generated by the system is comprised of three elements, concatenated together:

  1. The :extra option string supplied by the caller when invoking get_code/2. If supplied, it should remain the same each call, or if it does change, it should not change to a previously-used value. An example use case is the year as 2 digits. For most use cases, this piece can be ignored.
  2. A "uniquizer" string managed by CoderRing. If the number of codes generated in the seed is not exhausted, this will be an empty string. When the ring wraps around, the uniquizer will be incremented. Also, if get_code/2 is called with the bump: true, then this will be incremented and the ring cycle reset.
  3. The base code, coming directly from the seed data in the database.

Setup

Include CoderRing as a dependency in your application:

defp deps do
  [
    {:coder_ring, "~> 0.2.0"}
  ]
end

Configure code rings in your config.exs with something like:

config :my_app, MyApp.CoderRing,
  repo: MyApp.Repo,
  rings: [:widget, doodad: [base_length: 2]]

Here, we configured two code rings. Note that the :rings list may have atoms for default options or keyword list-style entries ({:doodad, base_length: 2}) if options are specified. See CoderRing.new/1 for available options.

Next, add the following to change/0 in a new or existing Ecto migration:

def change do
  CoderRing.Migration.change()
end

Make sure the database tables are seeded somewhere before use, whether in your existing seeds.exs or other application code:

MyApp.CoderRing.populate_rings_if_empty()

Finally, create a CoderRing module in your application. The section below explains the options.

Now you can use MyApp.CoderRing.get_code/2 to generate new codes.

iex> MyApp.CoderRing.get_code(:widget)
"7GRY"
iex> MyApp.CoderRing.get_code(:widget)
"PJ83"
iex> MyApp.CoderRing.get_code(:widget)
"NNW3"
iex> MyApp.CoderRing.get_code(:widget)
"Q5QA"

Creating a CoderRing Module for your Application

You'll want to create a module in your Application to expose the CoderRing functionality. Here are your options.

Stateless

This one is simplest, but it will require fetching current state from the database each time.

defmodule MyApp.CoderRing do
  use CoderRing, otp_app: :my_app
end

GenServer-based

You've chosen the BEAM for it's state-keeping prowess. In this option, a long-running GenServer is spawned which will hold the current state, avoiding the need to fetch state each time get_code/2 is called. Look out, however, if you're running with multiple servers as things will not work correctly if each node is running its own GenServer.

See CoderRing.GenRing for details on setting up this method.

Using a Global Process Registry

If your app is running on multiple nodes with Erlang clustering enabled, another option is to spawn a GenServer under a global process registry, named by the ring name, so that only one such process is allowed to run across the cluster.

For more details on this method, see Global Registry Method.

Dealing with an Unexpected Duplicate Code

If for some reason a code is returned which turns out not to be unique, it probably has to do with a previously-used "extra" string being used after switching to a different one in between. In this case, you may pass the bump: true option into get_code/2 to have the CoderRing begin using (or incrementing) the uniquizer. In this case, the ring cycle is also reset, and we should have another full cycle without conflicts.

Dealing with Timeouts

While I didn't have trouble loading my local Postgres in development with 1 million+ code records during initial population, I did find that when doing so on a deployment with the database over the network etc, I hit Ecto's default 15-second timeout.

To solve this, you might need to increase the timeout. Try:

MyApp.CoderRing.populate_rings_if_empty(timeout: :timer.minutes(2))

Increasing the :timeout and :ownership_timeout Repo configurations may also be needed.

Filtering Bad Words

If you wish to ensure that the generated codes do not include any profane words, also include expletive as a dependency in your application:

def deps do
  {:expletive, "~> 0.1.0"}
end

And then add the :expletive_blacklist option in the pertinent ring config:

config :my_app, MyApp.CoderRing,
  repo: MyApp.Repo,
  rings: [widget: [expletive_blacklist: :english]]

Acknowledgements

The methodology is appreciatively derived from Robert Gamble's accepted answer on this Stack Overflow page.

Link to this section Summary

Types

t()
  • :base_length - Number of characters to use in the base code.
  • :blacklist - Set this to :english to use the Expletive package's English word blacklist. Codes with occurrences of these words will be skipped in the database seeding step.
  • :memo - State-variable data, synced to the "code_memos" table.
  • :name - Name of this coder ring: .
  • :repo - Ecto.Repo module to use.

Callbacks

Invoke the name ring with the given message.

Functions

Get the memo from db for the given ring name.

Invoke the functionality identified by message. Memo should be loaded.

Load the relevant memo from the database into ring.

Make a new ring struct.

Load the ring into the database.

Load the ring into the database if it isn't already there.

For each ring, seed its data if it hasn't already been done.

Get a ring for module under otp_app by its name.

List all configured rings.

Link to this section Types

Specs

t() :: %CoderRing{
  base_length: non_neg_integer(),
  blacklist: atom(),
  memo: CoderRing.Memo.t() | nil,
  name: atom(),
  repo: module()
}
  • :base_length - Number of characters to use in the base code.
  • :blacklist - Set this to :english to use the Expletive package's English word blacklist. Codes with occurrences of these words will be skipped in the database seeding step.
  • :memo - State-variable data, synced to the "code_memos" table.
  • :name - Name of this coder ring: .
  • :repo - Ecto.Repo module to use.

Link to this section Callbacks

Specs

call(name :: atom(), message :: any()) :: any()

Invoke the name ring with the given message.

Link to this section Functions

Specs

codes_stream(t()) :: Enumerable.t()

Specs

get_max(t()) :: {CoderRing.Code.t(), non_neg_integer()}

Specs

get_memo(t()) :: CoderRing.Memo.t() | nil

Get the memo from db for the given ring name.

Specs

invoke(t(), message :: any()) :: {reply :: any(), t()}

Invoke the functionality identified by message. Memo should be loaded.

Specs

load_memo(t()) :: t()

Load the relevant memo from the database into ring.

Specs

new(keyword()) :: t()

Make a new ring struct.

Options

  • :name - Ring name atom. Required.
  • :base_length - Number of characters for the base code, 1-4. Default: 4
  • :repo - Ecto.Repo module to use. Required.
  • :expletive_blacklist - Expletive blacklist to use: :english, :international or nil. Note that, if enabled, the expletive package must be added as a dependency in your application. Default: nil
Link to this function

populate(ring, opts \\ [])

View Source

Specs

populate(
  t(),
  keyword()
) :: t()

Load the ring into the database.

All opts are passed along to Ecto.Repo calls to query and insert.

Link to this function

populate_if_empty(ring, opts \\ [])

View Source

Specs

populate_if_empty(
  t(),
  keyword()
) :: t()

Load the ring into the database if it isn't already there.

Link to this function

populate_rings_if_empty(rings, opts \\ [])

View Source

Specs

populate_rings_if_empty([t()], keyword()) :: :ok

For each ring, seed its data if it hasn't already been done.

See populate/2.

Link to this function

ring(otp_app, mod, name)

View Source

Specs

ring(atom(), module(), atom()) :: t() | nil

Get a ring for module under otp_app by its name.

Specs

rings(atom(), module()) :: [t()]

List all configured rings.

Memos will be unloaded. Use CoderRing.load_memo/1 to fetch state from the database.