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:
- The
:extra
option string supplied by the caller when invokingget_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. - 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 thebump: true
, then this will be incremented and the ring cycle reset. - 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
:base_length
- Number of characters to use in the base code.:blacklist
- Set this to:english
to use theExpletive
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 theExpletive
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
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 the functionality identified by message
. Memo should be loaded.
Specs
Load the relevant memo from the database into ring
.
Specs
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
ornil
. Note that, if enabled, theexpletive
package must be added as a dependency in your application. Default:nil
Specs
Load the ring
into the database.
All opts
are passed along to Ecto.Repo
calls to query and insert.
Specs
Load the ring into the database if it isn't already there.
Specs
For each ring, seed its data if it hasn't already been done.
See populate/2
.
Specs
Get a ring for module
under otp_app
by its name
.
Specs
List all configured rings.
Memos will be unloaded. Use CoderRing.load_memo/1
to fetch state from the
database.