ExPmtiles.Cache (ExPmtiles v0.3.4)

View Source

A GenServer implementation of a multi-level cache for PMTiles file data.

This GenServer provides efficient concurrent access to PMTiles map data through file-based caching mechanisms optimized for high-concurrency workloads.

Cache Levels

1. Directory Cache (File-based)

  • Caches deserialized directory structures from the PMTiles file
  • Stored as individual binary files on disk (one file per directory)
  • Root directory pre-fetched in background on startup
  • Leaf directories fetched on-demand as tiles are requested
  • Coordinates concurrent directory fetches to prevent duplicate S3 requests
  • No size limits - cache can grow indefinitely
  • No corruption issues from concurrent writes

2. Tile Cache (Optional File-based)

  • Optional persistent tile caching using individual files (disabled by default)
  • When enabled, each tile is stored as a separate file on disk
  • No size limits - cache can grow indefinitely
  • Maintains hit/miss statistics
  • File system handles concurrent access naturally

3. Concurrent Request Handling

  • Non-blocking tile fetching using spawned tasks
  • Prevents duplicate requests for the same tile
  • Coordinates multiple requests through ETS-based pending requests tables
  • Broadcasts results to all waiting processes
  • Handles errors gracefully with proper cleanup

Configuration

  • :name - Optional custom name for the GenServer (defaults to generated name)
  • :storage - Storage backend (:s3 or :local, auto-detected from bucket parameter)
  • :enable_dir_cache - Enable file-based directory caching (default: false)
  • :enable_tile_cache - Enable file-based tile caching (default: false)
  • :cache_dir - Directory for cache files (default: System.tmp_dir!() <> "/ex_pmtiles_cache")
  • :max_cache_age_ms - Maximum age of cache in milliseconds before automatic clearing (default: nil, disabled). When set, clears cache on GenServer startup and periodically based on age.
  • :cleanup_interval_ms - Interval for checking cache age in milliseconds (default: 1 hour)
  • :file_check_interval_ms - Interval for checking if source file has changed (default: 5 minutes). When the file changes (detected via ETag for S3 or mtime for local), cache is automatically cleared and repopulated.

Usage

# Start the cache for an S3 PMTiles file
{:ok, pid} = ExPmtiles.Cache.start_link(bucket: "maps", path: "map.pmtiles")

# Start the cache for a local PMTiles file
{:ok, pid} = ExPmtiles.Cache.start_link(bucket: nil, path: "/path/to/map.pmtiles")

# Start with automatic cache clearing after 24 hours
{:ok, pid} = ExPmtiles.Cache.start_link(
  bucket: "maps",
  path: "map.pmtiles",
  enable_tile_cache: true,
  max_cache_age_ms: :timer.hours(24),
  cleanup_interval_ms: :timer.hours(1)
)

# Start with custom file change detection interval
{:ok, pid} = ExPmtiles.Cache.start_link(
  bucket: "maps",
  path: "map.pmtiles",
  enable_dir_cache: true,
  file_check_interval_ms: :timer.minutes(1)  # Check for file changes every minute
)

# Get a tile by coordinates
case ExPmtiles.Cache.get_tile(pid, 10, 512, 256) do
  {:ok, tile_data} ->
    # Handle tile data
    tile_data
  {:error, reason} ->
    # Handle error
    nil
end

# Get cache statistics
stats = ExPmtiles.Cache.get_stats(pid)
# Returns: %{hits: 150, misses: 25}

# Manually clear the cache
ExPmtiles.Cache.clear_cache(pid)

Implementation Details

The GenServer maintains several tables and file structures:

  • Directory cache files: Individual binary files in cache_dir/directories/ (when enable_dir_cache: true)
  • Tile cache files: Individual binary files in cache_dir/tiles/ (when enable_tile_cache: true)
  • Pending tile table: (ETS) Tracks in-progress tile requests to prevent duplicates
  • Stats table: (ETS) Maintains hit/miss statistics
  • Pending directory table: (ETS) Coordinates concurrent directory fetches

Performance Features

  • Non-blocking init: GenServer starts immediately, ready to serve requests
  • Concurrent tile fetching: Multiple tiles can be fetched concurrently using tasks
  • File-based caching: Persistent storage with no size limits or corruption issues
  • Request deduplication: Multiple requests for the same tile/directory are coordinated
  • Scalable: Cache grows with your data without size constraints
  • Race condition handling: Safe concurrent writes to cache files
  • Automatic cache clearing: Optional time-based cache expiration with configurable intervals
  • Smart repopulation: Directory cache automatically repopulates after clearing
  • File change detection: Automatically detects when the source PMTiles file changes (via ETag for S3 or mtime for local files) and invalidates all caches, preventing stale or corrupted data from being served

Summary

Functions

Returns a specification to start this module under a supervisor.

Clears all cached data (tiles and directories) and resets statistics.

Retrieves cache statistics.

Retrieves a tile from the cache by coordinates.

Starts a new PMTiles cache GenServer.

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

clear_cache(server)

Clears all cached data (tiles and directories) and resets statistics.

This function removes all cached files from disk and resets hit/miss counters. It's useful for forcing a cache refresh or freeing up disk space.

When directory caching is enabled, this function will automatically trigger a background task to repopulate the directory cache after clearing. This ensures that frequently accessed directories are quickly available again.

Parameters

  • server - The cache process PID or registered name

Returns

  • :ok - Cache cleared successfully

Examples

iex> {:ok, pid} = ExPmtiles.Cache.start_link(bucket: "maps", path: "world.pmtiles")
iex> ExPmtiles.Cache.clear_cache(pid)
:ok

iex> ExPmtiles.Cache.clear_cache(:my_custom_cache)
:ok

get_stats(server)

Retrieves cache statistics.

Returns hit and miss counts for the cache, useful for monitoring cache performance and effectiveness.

Parameters

  • server - The cache process PID or registered name

Returns

  • %{hits: integer(), misses: integer()} - Cache statistics

Examples

iex> {:ok, pid} = ExPmtiles.Cache.start_link(bucket: "maps", path: "world.pmtiles")
iex> ExPmtiles.Cache.get_stats(pid)
%{hits: 150, misses: 25}

iex> # After some cache operations
iex> ExPmtiles.Cache.get_stats(pid)
%{hits: 175, misses: 30}

get_tile(server, z, x, y)

Retrieves a tile from the cache by coordinates.

This function handles both cache hits and misses. If the tile is not in the cache, it will be fetched from the PMTiles file and cached for future requests.

Parameters

  • server - One of:
    • {bucket, path} - Tuple identifying the PMTiles file (uses derived name)
    • pid - Process identifier for the cache GenServer
    • name - Atom representing the registered name of the cache GenServer
  • z - Zoom level (integer)
  • x - X coordinate (integer)
  • y - Y coordinate (integer)

Returns

  • {:ok, tile_data} - Tile data as binary
  • {:error, reason} - Error retrieving tile (e.g., :tile_not_found, :timeout)
  • nil - If the cache process is not available

Examples

iex> ExPmtiles.Cache.get_tile({"maps", "world.pmtiles"}, 10, 512, 256)
{:ok, <<...>>}

iex> ExPmtiles.Cache.get_tile({"maps", "world.pmtiles"}, 25, 0, 0)
{:error, :tile_not_found}

iex> ExPmtiles.Cache.get_tile(pid, 10, 512, 256)
{:ok, <<...>>}

iex> ExPmtiles.Cache.get_tile(:my_custom_cache, 10, 512, 256)
{:ok, <<...>>}

start_link(opts \\ [])

Starts a new PMTiles cache GenServer.

Creates a named GenServer process that manages caching for a specific PMTiles file. The process name is derived from the bucket and path to ensure uniqueness, unless a custom name is provided.

Parameters

  • opts - Keyword list of options:
    • :bucket - S3 bucket name (or nil for local files)
    • :path - Path to the PMTiles file
    • :name - Custom name for the GenServer (atom). If not provided, a name will be derived from bucket and path
    • :enable_dir_cache - Enable file-based directory caching (default: false)
    • :enable_tile_cache - Enable file-based tile caching (default: false)
    • :max_cache_age_ms - Maximum age of cache before automatic clearing (default: nil, disabled). When set, clears cache on startup.
    • :cleanup_interval_ms - Interval for checking cache age (default: 1 hour)
    • :file_check_interval_ms - Interval for checking if source file has changed (default: 5 minutes)
    • :exaws_config - ExAws config for testing (default: nil, uses default config)

Returns

  • {:ok, pid} - Successfully started cache process
  • {:error, reason} - Failed to start cache

Examples

iex> ExPmtiles.Cache.start_link(region: nil, bucket: "maps", path: "world.pmtiles")
{:ok, #PID<0.123.0>}

iex> ExPmtiles.Cache.start_link(bucket: nil, path: "/data/local.pmtiles")
{:ok, #PID<0.124.0>}

iex> ExPmtiles.Cache.start_link(bucket: "maps", path: "world.pmtiles", enable_tile_cache: true)
{:ok, #PID<0.125.0>}

iex> ExPmtiles.Cache.start_link(bucket: "maps", path: "world.pmtiles", name: :my_custom_cache, enable_dir_cache: true)
{:ok, #PID<0.126.0>}