Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0193b49
Correct indentation
dan-fernandes Nov 13, 2025
a460794
Move role insertion out of scope of table creation
dan-fernandes Nov 14, 2025
49957eb
Fix check for in-memory db
dan-fernandes Nov 14, 2025
9d36d5a
Remove seemingly useless db commit
dan-fernandes Nov 19, 2025
4dca8a9
Skip invalid metrics for StaticPool
dan-fernandes Nov 19, 2025
be9a434
Merge branch 'main' into fix-database-settings
dan-fernandes Nov 19, 2025
22280af
Cache in-memory databases
dan-fernandes Nov 19, 2025
2baf8a7
Pre-commit fixes
dan-fernandes Nov 19, 2025
ba2569e
Add test for in-memory authn
dan-fernandes Nov 19, 2025
4c71dd6
Pre-commit fixes
dan-fernandes Nov 19, 2025
dd602ad
Remove unused global variables
dan-fernandes Nov 19, 2025
9339ab4
Add option to not used cached databases
dan-fernandes Nov 19, 2025
245b5f7
Revert "Add option to not used cached databases"
danielballan Nov 19, 2025
3174de9
In tests with multiple databases, use file to avoid colliding in-memory.
danielballan Nov 19, 2025
dd71251
Use distinct named memory databases instead of on-disk.
danielballan Nov 20, 2025
52dd6ce
Improve checking of in-memory SQLite database.
danielballan Nov 20, 2025
1060690
Use random named memory name to avoid inter-module collisions
danielballan Nov 20, 2025
0427eec
Recognize mode=memory query parameter
danielballan Nov 20, 2025
e6dc79c
Remove database settings
dan-fernandes Nov 20, 2025
dccd4a1
Fix is_memory_sqlite conditional on non-existent property
dan-fernandes Nov 20, 2025
8091a83
Stop caching in-memory databases
dan-fernandes Nov 20, 2025
6e55003
Auto-format
dan-fernandes Nov 20, 2025
2d2e049
More formatting
dan-fernandes Nov 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions tiled/_tests/test_configs/config_in_memory_authn.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# config.yml
trees:
- path: /
tree: tiled.examples.generated_minimal:tree
uvicorn:
host: 0.0.0.0
port: 8000
authentication:
providers:
- provider: test
authenticator: tiled.authenticators:DictionaryAuthenticator
args:
users_to_passwords:
alice: PASSWORD
secret_keys:
- SECRET
26 changes: 26 additions & 0 deletions tiled/_tests/test_in_memory_authn.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from pathlib import Path

import yaml

from tiled._tests.utils import enter_username_password
from tiled.client import Context, from_context
from tiled.server.app import build_app_from_config

here = Path(__file__).parent.absolute()


def test_good_path():
"""Test authn database defaults to in-memory catalog"""
with open(here / "test_configs" / "config_in_memory_authn.yml") as config_file:
config = yaml.load(config_file, Loader=yaml.BaseLoader)

app = build_app_from_config(config)
context = Context.from_app(app)

with enter_username_password("alice", "PASSWORD"):
client = from_context(context, remember_me=False)

client.logout()
context.close()

assert True
2 changes: 1 addition & 1 deletion tiled/_tests/test_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
def client_factory(readable_storage=None):
with tempfile.TemporaryDirectory() as tempdir:
catalog = in_memory(
writable_storage=str(tempdir), readable_storage=readable_storage
writable_storage=str(tempdir), readable_storage=readable_storage, use_cached_database=False
)
app = build_app(catalog)
with Context.from_app(app) as context:
Expand Down
9 changes: 4 additions & 5 deletions tiled/authn_database/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,13 @@ async def create_default_roles(db):


async def initialize_database(engine: AsyncEngine) -> None:
async with engine.connect() as conn:
async with engine.begin() as conn:
# Create all tables.
await conn.run_sync(Base.metadata.create_all)
await conn.commit()

# Initialize Roles table.
async with AsyncSession(engine) as db:
await create_default_roles(db)
# Initialize Roles table.
async with AsyncSession(engine) as db:
await create_default_roles(db)


async def purge_expired(db: AsyncSession, cls) -> int:
Expand Down
7 changes: 6 additions & 1 deletion tiled/catalog/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,9 @@ def __init__(
key_maker=lambda: str(uuid.uuid4()),
storage_pool_size=5,
storage_max_overflow=10,
use_cached_database=True,
):
self.engine = get_database_engine(database_settings)
self.engine = get_database_engine(database_settings, use_cached_database=use_cached_database)
self.database_settings = database_settings
self.writable_storage = {}
self.readable_storage = {}
Expand Down Expand Up @@ -1676,6 +1677,7 @@ def in_memory(
adapters_by_mimetype=None,
top_level_access_blob=None,
cache_settings=None,
use_cached_database=True
):
if not named_memory:
uri = "sqlite:///:memory:"
Expand All @@ -1693,6 +1695,7 @@ def in_memory(
adapters_by_mimetype=adapters_by_mimetype,
top_level_access_blob=top_level_access_blob,
cache_settings=cache_settings,
use_cached_database=use_cached_database
)


Expand All @@ -1712,6 +1715,7 @@ def from_uri(
storage_pool_size=5,
catalog_max_overflow=10,
storage_max_overflow=10,
use_cached_database=True
):
uri = ensure_specified_sql_driver(uri)
if init_if_not_exists:
Expand Down Expand Up @@ -1747,6 +1751,7 @@ def from_uri(
cache_settings,
storage_pool_size=storage_pool_size,
storage_max_overflow=storage_max_overflow,
use_cached_database=use_cached_database,
)
node = RootNode(metadata, specs, top_level_access_blob)
mount_path = (
Expand Down
14 changes: 7 additions & 7 deletions tiled/server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,13 +468,13 @@ def override_get_settings():
settings.database_settings.max_overflow = database.max_overflow
if database.init_if_not_exists is not None:
settings.database_init_if_not_exists = database.init_if_not_exists
if authenticators:
# If we support authentication providers, we need a database, so if one is
# not set, use a SQLite database in memory. Horizontally scaled deployments
# must specify a persistent database.
settings.database_settings.uri = (
settings.database_settings.uri or "sqlite://"
)
if authenticators:
# If we support authentication providers, we need a database, so if one is
# not set, use a SQLite database in memory. Horizontally scaled deployments
# must specify a persistent database.
settings.database_settings.uri = (
settings.database_settings.uri or "sqlite://"
)
if (
authenticators
and len(authenticators) == 1
Expand Down
15 changes: 9 additions & 6 deletions tiled/server/connection_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,11 @@ async def __aexit__(self, *excinfo):


def open_database_connection_pool(database_settings: DatabaseSettings) -> AsyncEngine:
if make_url(database_settings.uri).database == ":memory:":
if (
make_url(database_settings.uri).database == ":memory:"
or database_settings.uri == "sqlite://"
):
# For SQLite databases that exist only in process memory,
# pooling is not applicable. Just return an engine and don't cache it.
engine = create_async_engine(
ensure_specified_sql_driver(database_settings.uri),
echo=DEFAULT_ECHO,
Expand All @@ -77,9 +79,9 @@ def open_database_connection_pool(database_settings: DatabaseSettings) -> AsyncE
pool_pre_ping=database_settings.pool_pre_ping,
)

# Cache the engine so we don't create more than one pool per database_settings.
monitor_db_pool(engine.pool, sanitize_uri(database_settings.uri)[0])
_connection_pools[database_settings] = engine
# Cache the engine so we don't create more than one pool per database_settings.
monitor_db_pool(engine.pool, sanitize_uri(database_settings.uri)[0])
_connection_pools[database_settings] = engine

# For SQLite, ensure that foreign key constraints are enforced.
if engine.dialect.name == "sqlite":
Expand All @@ -96,14 +98,15 @@ async def close_database_connection_pool(database_settings: DatabaseSettings):

def get_database_engine(
settings: Union[Settings, DatabaseSettings] = Depends(get_settings),
use_cached_database = True,
) -> AsyncEngine:
database_settings = (
settings.database_settings if isinstance(settings, Settings) else settings
)
# Special case for single-user mode
if database_settings.uri is None:
return None
if database_settings in _connection_pools:
if database_settings in _connection_pools and use_cached_database:
return _connection_pools[database_settings]
else:
return open_database_connection_pool(database_settings)
Expand Down
6 changes: 5 additions & 1 deletion tiled/server/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from prometheus_client import Counter, Gauge, Histogram
from sqlalchemy import event
from sqlalchemy.pool import QueuePool
from sqlalchemy.pool import QueuePool, StaticPool

REQUEST_DURATION = Histogram(
"tiled_request_duration_seconds",
Expand Down Expand Up @@ -220,6 +220,10 @@ def on_checkout(dbapi_connection, connection_record, connection_proxy):
DB_POOL_CHECKEDOUT.labels(name).inc()
DB_POOL_CHECKOUTS_TOTAL.labels(name).inc()

# Skip for single-connection database:
if isinstance(pool, StaticPool):
return

# First overflow: we just used the very first overflow slot
if pool.overflow() == 1:
DB_POOL_FIRST_OVERFLOW_TOTAL.labels(name).inc()
Expand Down
Loading