TzWorld.Backend.SpatialIndex (TzWorld v2.5.0)

Copy Markdown View Source

Resolves timezones from a coordinate using an R-tree spatial index held in :persistent_term.

This is the recommended backend and is also the default — applications that do not pin :default_backend will pick it up automatically as long as it is started in the supervision tree.

How it works

At startup the backend reads the compressed timezone shape data that mix tz_world.update installs and builds two structures:

  • a Sort-Tile-Recursive packed R-tree over every shape's bounding box, with one entry per sub-polygon of a Geo.MultiPolygon;

  • each shape compiled by TzWorld.PackedGeometry: its rings packed into binaries, with their edges indexed by horizontal band.

Both are stored with the data version as a single term under one :persistent_term key. A lookup finds candidate shapes in the R-tree, then tests each against only the ring edges in the query point's band — typically a few dozen, even in rings with nearly 200,000 vertices — so it takes microseconds.

Lookups (TzWorld.timezone_at/1 and TzWorld.all_timezones_at/1) read the stored term directly. They do not go through the GenServer mailbox and do not copy any term, which makes them lock-free and safe to call from any number of processes concurrently.

Reload

reload_timezone_data/0 rebuilds the index and replaces the stored term in one :persistent_term.put/2. Lookups carry on against the previous index until then, so a reload never blocks them. Loading and reloading compile the shapes in parallel, one task per shape, and take a couple of seconds on a multi-core machine and several on a single core. The put also triggers a global garbage collection for every process that references a persistent term — fine when reload is invoked after a data update, costly on a hot path. Reload should not be called on every request.

Memory profile

About 150 MB for the data without ocean coverage, the default. Ring vertices are held as packed binaries at 16 bytes a vertex, and those binaries live off-heap: :persistent_term.info/0 reports only the small term that refers to them, so measure :erlang.memory/1 instead.

Public API

In normal use you will not call functions on this module directly — use TzWorld.timezone_at/1, TzWorld.all_timezones_at/1, TzWorld.version/0, and TzWorld.reload_timezone_data/0. Add this module to your supervision tree to make it the running backend:

children = [
  TzWorld.Backend.SpatialIndex
]

Summary

Functions

Returns a specification to start this module under a supervisor.

Starts the backend and loads the time zone data.

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

start_link(options \\ [])

Starts the backend and loads the time zone data.

The load is performed synchronously inside init/1, so once start_link/1 returns the index is in :persistent_term and lookups are immediately ready to serve. The backend is registered under its module name.

Arguments

  • options is a keyword list of options.

Options

  • There are none. options is accepted so the module can be used as a child specification, and is otherwise ignored.

Returns

  • {:ok, pid} once the data is loaded. If the data has not been installed the backend still starts and logs a warning, and lookups return {:error, :enoent} until the data is installed with mix tz_world.update and loaded with TzWorld.reload_timezone_data/0.

  • {:error, {:already_started, pid}} if the backend is already running.

  • {:error, reason} if the installed data could not be decoded.

Examples

Add the backend to an application's supervision tree:

children = [
  TzWorld.Backend.SpatialIndex
]

Supervisor.start_link(children, strategy: :one_for_one)