Skip to content

feat: Thread-safe mode for multi-tenant applications#17

Closed
dimitri-yatsenko wants to merge 13 commits intomasterfrom
feature/thread-safe-config
Closed

feat: Thread-safe mode for multi-tenant applications#17
dimitri-yatsenko wants to merge 13 commits intomasterfrom
feature/thread-safe-config

Conversation

@dimitri-yatsenko
Copy link
Copy Markdown
Owner

@dimitri-yatsenko dimitri-yatsenko commented Feb 13, 2026

Summary

Adds opt-in thread-safe mode that blocks global state access, requiring explicit connection management for multi-tenant web applications and concurrent processing.

📄 Design Specification

Key Features

Thread-Safe Mode (thread_safe setting)

  • Read-only after initialization: Can only be set via DJ_THREAD_SAFE environment variable or datajoint.json config file
  • When enabled, blocks all dj.config access except reading thread_safe itself
  • Blocks dj.conn() singleton to prevent shared connection state

New Connection API

  • Connection.from_config(): Creates connections with explicit configuration
    • Works identically whether thread_safe is on or off
    • Never falls back to global config (uses defaults for unspecified values)
    • Accepts all connection-scoped settings as parameters

Connection-Scoped Configuration (conn.config)

  • ConnectionConfig class for per-connection settings
  • Read/write access to: safemode, database_prefix, stores, cache, display_limit, etc.
  • conn.config.override(): Context manager for temporary setting changes
  • Legacy API (dj.conn()) falls back to global config for backward compatibility

Usage

# Enable thread-safe mode (deployment-time decision)
# Option 1: Environment variable
export DJ_THREAD_SAFE=true

# Option 2: datajoint.json
{"thread_safe": true}

# Create connections explicitly
conn = dj.Connection.from_config(
    host="localhost",
    user="root", 
    password="secret",
    safemode=False,
    display_limit=25
)

# Access connection-scoped settings
conn.config.safemode = False
conn.config.display_limit = 50

# Temporary overrides
with conn.config.override(safemode=False):
    # safemode disabled here
    pass
# safemode restored

# Schema requires explicit connection
schema = dj.Schema("mydb", connection=conn)

Breaking Changes

None. Thread-safe mode is opt-in and disabled by default. Existing code continues to work unchanged.

Other Fixes

  • Pydantic validation: Fixed Config.__setattr__ to preserve Pydantic's validate_assignment=True
  • Removed dead code: enable_python_native_blobs setting (was defined but never read)
  • Removed charset setting: Database charset should be configured on the server, not client

Files Changed

  • src/datajoint/settings.py - Thread-safe guards, read-only thread_safe
  • src/datajoint/connection.py - ConnectionConfig, from_config(), override()
  • src/datajoint/schema.py - Require explicit connection in thread-safe mode
  • src/datajoint/errors.py - ThreadSafetyError exception
  • tests/unit/test_thread_safe.py - Comprehensive test suite
  • tests/unit/test_settings.py - Updated tests for new behavior
  • docs/design/thread-safe-mode.md - Design specification

Test Plan

  • All 270 unit tests pass
  • Integration tests pass
  • Manual testing with multi-tenant scenario

🤖 Generated with Claude Code

dimitri-yatsenko and others added 11 commits February 13, 2026 11:49
This adds opt-in thread-safe mode that blocks global state access,
requiring explicit connection management for multi-tenant web apps.

Changes:
- Add ThreadSafetyError exception
- Add thread_safe setting (default False, can be set via DJ_THREAD_SAFE env)
- Guard config dict access (__getitem__, __setitem__) when thread_safe=True
- Guard dj.conn() singleton when thread_safe=True
- Add Connection.from_config() class method for explicit connection creation
- Add backend parameter to Connection.__init__
- Convert config dict access to attribute access in Connection methods

When thread_safe=True:
- dj.config["..."] raises ThreadSafetyError
- dj.conn() raises ThreadSafetyError
- Connection.from_config() always works (reads config at call time)

Usage:
  dj.config.thread_safe = True  # or DJ_THREAD_SAFE=true
  conn = dj.Connection.from_config(tenant_config)
  schema = dj.Schema("mydb", connection=conn)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- thread_safe is a regular setting (env var, config file, programmatic)
- Once True, cannot be set back to False (one-way lock)
- When enabled, all global config access blocked except thread_safe itself
- Allow Pydantic model_ methods during initialization
- Tests use object.__setattr__ to bypass lock for reset

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Design document covering:
- One-way lock for thread_safe setting
- Only thread_safe accessible on dj.config in thread-safe mode
- ConnectionConfig with read/write access, forwarding to global when off
- Universal API (Connection.from_config + conn.config) works in both modes
- Migration path and backward compatibility

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This setting was defined but never read by any code - dead code.
Removed from:
- Config class definition
- Template generation
- Test that used it (test still validates error suppression)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Database charset/encoding should be configured on the server,
not the client. Removes:
- charset from ConnectionSettings
- charset from connection template
- charset parameter from adapter connect calls
- charset from thread-safe mode spec

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add ConnectionConfig class with read/write settings
- Connection.config forwards to global when thread_safe=False
- Connection.config uses defaults when thread_safe=True
- Connection.from_config() accepts all connection-scoped settings
- Schema.create_tables property defers to connection.config
- Schema requires explicit connection in thread-safe mode
- Add create_tables to ConnectionConfig
- Update spec with access patterns (schema.connection.config)
- Add comprehensive tests for all components

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- thread_safe is now read-only after initialization
  - Can only be set via DJ_THREAD_SAFE env var or datajoint.json
  - Programmatic setting raises ThreadSafetyError

- ConnectionConfig now uses _use_global_fallback instead of _thread_safe
  - New API (from_config) uses defaults, never falls back to global
  - Legacy API (dj.conn) falls back to global for backward compat
  - Behavior is based on which API was used, not thread_safe flag

- Updated tests and spec document

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Updated documentation to reflect that thread_safe is read-only after
initialization and can only be set via DJ_THREAD_SAFE environment
variable or datajoint.json config file, not programmatically.

Removed references to "one-way lock" pattern which is no longer accurate.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Added `conn.config.override()` method to temporarily change connection-
scoped settings. This provides the same functionality as `dj.config.override()`
but works in thread-safe mode where global config access is blocked.

Example:
    with conn.config.override(safemode=False, display_limit=50):
        # settings changed temporarily
        pass
    # settings restored

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Changed from object.__setattr__() to super().__setattr__() so that
Pydantic's validate_assignment=True works correctly. This fixes:
- test_loglevel_validation - now correctly rejects invalid loglevels
- test_fetch_format_validation - now correctly rejects invalid formats
- test_cache_path_string - now correctly coerces strings to Path

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@dimitri-yatsenko dimitri-yatsenko changed the title Feature/thread safe config feat: Thread-safe mode for multi-tenant applications Feb 13, 2026
dimitri-yatsenko and others added 2 commits February 13, 2026 15:36
Removed init_function from ConnectionSettings and all connection
code paths. This was dead code - a pass-through to PyMySQL's
init_command that was never used by DataJoint.

Removed from:
- ConnectionSettings class (now removed entirely)
- Config.connection field
- Template generation
- dj.conn() function
- Connection.__init__
- Connection.from_config
- MySQL adapter
- Thread-safe mode spec

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Renamed ConnectionConfig to ConnectionSettings
- Removed duplicate _DEFAULTS dict
- Added _get_default() to read defaults from Config Pydantic field definitions
- Changed constructor signature: ConnectionSettings(values=dict, use_global_fallback=bool)
- Updated tests to use new constructor pattern

This eliminates duplication of setting definitions, validation, and defaults
between Config and ConnectionSettings classes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@dimitri-yatsenko dimitri-yatsenko marked this pull request as draft February 13, 2026 21:57
@dimitri-yatsenko
Copy link
Copy Markdown
Owner Author

closing for a more elegant solution

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.

1 participant