Skip to content

Add data caching system with disk persistence (Issue #50 Task 2.1)#58

Merged
SBFRF merged 7 commits into
mainfrom
feature/issue-50-cache-server-detection
Jul 16, 2026
Merged

Add data caching system with disk persistence (Issue #50 Task 2.1)#58
SBFRF merged 7 commits into
mainfrom
feature/issue-50-cache-server-detection

Conversation

@SBFRF

@SBFRF SBFRF commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Summary

  • Implements disk-based data caching for murgtools to avoid repeated network requests
  • Caching is OFF by default - must be explicitly enabled
  • Addresses Issue [Phase 2] Performance Optimizations #50 Task 2.1 (Cache Server Detection) plus full data caching infrastructure

Features

Feature Implementation
Default state OFF - must be explicitly enabled
Enable via code getObs(d1, d2, use_cache=True)
Enable via env MURGTOOLS_CACHE_ENABLED=1
Cache location /data/getdata (configurable via cache_dir or MURGTOOLS_CACHE_DIR)
TTL 180 days / 6 months (configurable via cache_ttl_days or MURGTOOLS_CACHE_TTL_DAYS)
Force refresh method(..., force_refresh=True)
Thread safety RLock for concurrent access

Methods with Caching Support

  • getWaveData - Wave measurements
  • getCurrents - Ocean currents
  • getWind - Wind data
  • getWL - Water level (NOAA tide)
  • getGaugeWL - Gauge water level
  • getCTD - CTD measurements
  • getALT - Altimeter data
  • getLidarRunup - Lidar runup
  • getBathyTransectFromNC - Bathymetry transects

Usage Example

from murgtools import getObs
import datetime as DT

# Enable caching
obs = getObs(
    DT.datetime(2024, 1, 1),
    DT.datetime(2024, 1, 31),
    use_cache=True,
    cache_dir='/data/getdata',
    cache_ttl_days=180,
)

# First call fetches from network and caches
waves = obs.getWaveData(gaugenumber='waverider-26m')

# Subsequent calls return cached data (fast!)
waves = obs.getWaveData(gaugenumber='waverider-26m')

# Force refresh bypasses cache
waves = obs.getWaveData(gaugenumber='waverider-26m', force_refresh=True)

Changes

  • murgtools/config.py: Add cache configuration constants and thread-safe server detection caching
  • murgtools/cache.py: New module with DataCache class and helper functions
  • murgtools/getdata/getDataFRF.py: Add caching support to getObs class and all major get methods
  • tests/test_config.py: Add 9 tests for server caching
  • tests/test_cache.py: Add 39 tests for data caching

Test plan

  • All 193 existing tests pass
  • 9 new server caching tests pass
  • 39 new data caching tests pass
  • Thread safety tested with concurrent access
  • Numpy type serialization handled

🤖 Generated with Claude Code

SBFRF and others added 4 commits July 16, 2026 08:58
Previously, get_thredds_server() ran socket.gethostbyname() on every call
to detect FRF vs external network. This adds module-level caching so the
IP address and server selection are computed once at first use.

Thread safety improvements:
- Use threading.RLock (reentrant lock) to prevent deadlock when
  get_thredds_server() calls _get_cached_ip() while holding the lock
- Double-checked locking pattern for safe concurrent initialization
- Catch OSError (Python 3 standard) instead of socket.error

Input validation:
- Validate ip_address is a string to prevent type confusion attacks
- Return safe defaults (empty string, False) for invalid inputs

Changes:
- Add _cache_lock (RLock), _cached_ip_address, _cached_server_result
- Add _get_cached_ip() helper for lazy thread-safe IP detection
- Modify get_thredds_server() to return cached result for default case
- Modify is_frf_network() to use cached IP with type validation
- Add clear_server_cache() for testing and network change scenarios
- Add 9 tests for caching behavior including thread safety

Addresses issue #50 task 2.1.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements a comprehensive data caching system for murgtools that stores
retrieved data to disk to avoid repeated network requests.

Features:
- Caching is OFF by default, must be explicitly enabled
- Enable via code: enable_cache() or DataCache(enabled=True)
- Enable via environment: MURGTOOLS_CACHE_ENABLED=1
- Default cache directory: /data/getdata (configurable)
- Default TTL: 180 days / 6 months (configurable)
- Force refresh: cache.get(..., force_refresh=True)
- Clear all or by age: clear_cache(older_than_days=30)
- Thread-safe with RLock

Configuration options:
- cache_dir: Custom cache directory path
- ttl_days: Time-to-live in days (0 = never expire)
- Environment variables for all settings

New files:
- murgtools/cache.py: DataCache class and helper functions
- tests/test_cache.py: 34 comprehensive tests

Addresses issue #50 (partial - data caching infrastructure).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds caching support to getObs for caching retrieved oceanographic data
to disk. Caching is OFF by default and must be explicitly enabled.

getObs class changes:
- Add use_cache, cache_dir, cache_ttl_days parameters to __init__
- Add force_refresh parameter to control cache bypass
- Add _get_with_cache() helper method for cache integration
- Refactor getWaveData() to use caching via _getWaveData_impl()

Cache module improvements:
- Add NumpyJSONEncoder for handling numpy types in metadata
- Fix JSON serialization of numpy int64/float64 values

Usage example:
    obs = getObs(d1, d2, use_cache=True, cache_dir='/data/getdata')
    data = obs.getWaveData(gaugenumber='waverider-26m')
    # Second call returns cached data:
    data = obs.getWaveData(gaugenumber='waverider-26m')
    # Force refresh:
    data = obs.getWaveData(gaugenumber='waverider-26m', force_refresh=True)

Tests:
- Add 5 integration tests for getObs caching

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Extends caching integration to all commonly used data retrieval methods
in the getObs class. Each method now accepts force_refresh parameter.

Methods with caching support:
- getWaveData (already done in previous commit)
- getCurrents - ocean currents data
- getWind - wind measurements
- getWL - water level (NOAA tide)
- getGaugeWL - gauge water level
- getCTD - CTD measurements
- getALT - altimeter data
- getLidarRunup - lidar runup measurements
- getBathyTransectFromNC - bathymetry transects

Each method follows the same pattern:
1. Add force_refresh parameter
2. Create _impl method with actual logic
3. Wrap with _get_with_cache helper

Usage:
    obs = getObs(d1, d2, use_cache=True)

    # All these calls are now cacheable:
    waves = obs.getWaveData(gaugenumber='waverider-26m')
    currents = obs.getCurrents(gaugenumber='awac-11m')
    wind = obs.getWind()
    wl = obs.getWL()
    bathy = obs.getBathyTransectFromNC()

    # Force refresh any cached data:
    waves = obs.getWaveData(gaugenumber='waverider-26m', force_refresh=True)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 16, 2026 15:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces an opt-in, disk-persistent caching layer to reduce repeated network requests in murgtools, and adds module-level caching for THREDDS server detection (Issue #50 Task 2.1). It also updates getObs retrieval methods to support cached fetches with TTL and a per-call force_refresh override, plus comprehensive unit tests.

Changes:

  • Add murgtools.cache with a disk-backed DataCache (TTL, force refresh, thread-safety) and convenience helpers exported at package level.
  • Cache THREDDS server auto-detection results in murgtools.config with a thread-safe module-level cache and test coverage.
  • Integrate optional caching into getObs methods in getDataFRF.py and add extensive tests for cache behavior.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
murgtools/config.py Adds thread-safe module-level caching for THREDDS server auto-detection and cache reset helper.
murgtools/cache.py New disk-persistent caching implementation (metadata + pickle payload) with TTL and global-cache helpers.
murgtools/getdata/getDataFRF.py Adds use_cache/TTL configuration to getObs and wraps major getters with a cache helper.
murgtools/__init__.py Exposes cache APIs (DataCache, get_cache, enable_cache, disable_cache, clear_cache) at package import level.
tests/test_config.py Adds server-detection caching tests (including thread-safety expectations).
tests/test_cache.py Adds extensive unit tests for DataCache, env var config, global cache helpers, and getObs integration.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread murgtools/getdata/getDataFRF.py Outdated
Comment thread murgtools/cache.py Outdated
Comment on lines +57 to +64
def default(self, obj):
if isinstance(obj, np.integer):
return int(obj)
if isinstance(obj, np.floating):
return float(obj)
if isinstance(obj, np.ndarray):
return obj.tolist()
return super().default(obj)
Comment thread murgtools/cache.py
Comment on lines +253 to +257
if extra_params:
# Sort keys for deterministic ordering
sorted_params = sorted(extra_params.items())
key_parts.append(json.dumps(sorted_params, sort_keys=True))

Comment thread murgtools/cache.py Outdated
Comment on lines +295 to +309
with self._lock:
# Check cache unless force_refresh is requested
if not force_refresh:
cached_data = self._read_cache(cache_key)
if cached_data is not None:
return cached_data

# Fetch fresh data
data = fetch_func()

# Store in cache
if data is not None:
self._write_cache(cache_key, data, data_source, time_range, extra_params)

return data
Fixes:
1. Remove unused `get_cache` import from getDataFRF.py
2. NumpyJSONEncoder: Add fallback to str() for unknown types (datetime, Path, etc.)
3. _generate_cache_key: Use NumpyJSONEncoder for consistent serialization
4. DataCache.get(): Release lock during fetch_func() to allow concurrent fetches

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 9 changed files in this pull request and generated 6 comments.

Comment thread murgtools/cache.py Outdated
Comment thread murgtools/cache.py
Comment on lines +337 to +339
# Check if cache files exist
if not data_path.exists() or not meta_path.exists():
return None
Comment thread murgtools/cache.py
Comment on lines +353 to +356
# Read data
with open(data_path, 'rb') as f:
data = pickle.load(f)

Comment thread murgtools/config.py Outdated
Comment thread .mcp-debug-tools/config.json Outdated
Comment on lines +1 to +5
{
"vscodeInstanceId": "vscode-2474-1774366165262",
"port": 8890,
"pid": 2474,
"workspacePath": "/home/slug/repos/murgTools",
Comment thread .claude/settings.json Outdated
Comment on lines +1 to +5
{
"permissions": {
"allow": [
"Bash(gh pr *)",
"Bash(pip show *)",

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 9 changed files in this pull request and generated 7 comments.

Comment thread murgtools/getdata/getDataFRF.py Outdated
Comment on lines +321 to +325
# Include server in extra_params for cache key
cache_params = {'server': self.server}
if extra_params:
cache_params.update(extra_params)

Comment thread murgtools/cache.py Outdated
Comment on lines +182 to +194
# Determine TTL
if ttl_days is not None:
self._ttl_days = ttl_days
else:
env_ttl = os.environ.get(config.ENV_CACHE_TTL_DAYS)
if env_ttl:
try:
self._ttl_days = int(env_ttl)
except ValueError:
logger.warning(f"Invalid TTL value '{env_ttl}', using default")
self._ttl_days = config.DEFAULT_CACHE_TTL_DAYS
else:
self._ttl_days = config.DEFAULT_CACHE_TTL_DAYS
Comment thread murgtools/cache.py
Comment on lines +238 to +248
def _ensure_cache_dir(self):
"""Create cache directory if it doesn't exist."""
try:
self._cache_dir.mkdir(parents=True, exist_ok=True)
except PermissionError as e:
logger.warning(f"Cannot create cache directory {self._cache_dir}: {e}")
logger.warning("Caching will be disabled")
self._enabled = False
except OSError as e:
logger.warning(f"Error creating cache directory {self._cache_dir}: {e}")
self._enabled = False
Comment thread murgtools/cache.py Outdated
Comment thread tests/test_cache.py
Comment thread .mcp-debug-tools/config.json Outdated
Comment on lines +1 to +7
{
"vscodeInstanceId": "vscode-2474-1774366165262",
"port": 8890,
"pid": 2474,
"workspacePath": "/home/slug/repos/murgTools",
"workspaceName": "murgTools"
} No newline at end of file
Comment thread .claude/settings.json Outdated
Comment on lines +1 to +10
{
"permissions": {
"allow": [
"Bash(gh pr *)",
"Bash(pip show *)",
"Bash(find /home/slug/repos/murgTools -maxdepth 3 -name \"*.tif\" 2>&1 | head -20; ls /tmp/*.tif 2>/dev/null | head -5)",
"Read(//tmp/**)"
]
}
}
SBFRF and others added 2 commits July 16, 2026 15:37
- Remove unused timedelta import from cache.py
- Fix thread safety in _get_cached_ip() by always using lock
  (clear_server_cache can reset the value, so lock-free fast path was unsafe)
- Remove accidentally committed .claude/ and .mcp-debug-tools/ directories
- Add .claude/ and .mcp-debug-tools/ to .gitignore

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove unused pickle import from test_cache.py
- Use TTL setter in __init__ for validation (prevents negative TTL)
- Include resolved server URL in cache key (not just self.server which
  can be None for auto-detect, causing cache collisions across networks)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@SBFRF
SBFRF merged commit 6656d2c into main Jul 16, 2026
4 checks passed
@SBFRF
SBFRF deleted the feature/issue-50-cache-server-detection branch July 16, 2026 19:58
@SBFRF SBFRF mentioned this pull request Jul 16, 2026
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants