Skip to content

Commit

Permalink
Merge pull request #1160 from lsst-sqre/tickets/DM-47716a
Browse files Browse the repository at this point in the history
DM-47716: Avoid creating Firestore clients on every request
  • Loading branch information
rra authored Nov 21, 2024
2 parents eb3e2c6 + 1b597e2 commit fa7896e
Show file tree
Hide file tree
Showing 4 changed files with 37 additions and 20 deletions.
3 changes: 3 additions & 0 deletions changelog.d/20241120_164500_rra_DM_47716_queue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Bug fixes

- Avoid creating a Google Firestore client for every request, since it does authentication setup on creation. Instead, create a single client that will be used for all requests.
1 change: 1 addition & 0 deletions docs/documenteer.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ nitpick_ignore = [
# by having Sphinx complain about a new symbol.
["py:class", "dataclasses_avroschema.pydantic.main.AvroBaseModel"],
["py:class", "dataclasses_avroschema.main.AvroModel"],
["py:class", "google.cloud.firestore_v1.async_client.AsyncClient"],
["py:class", "fastapi.applications.FastAPI"],
["py:class", "fastapi.datastructures.DefaultPlaceholder"],
["py:class", "fastapi.exceptions.HTTPException"],
Expand Down
14 changes: 12 additions & 2 deletions src/gafaelfawr/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import structlog
from bonsai import LDAPClient
from bonsai.asyncio import AIOConnectionPool
from google.cloud import firestore
from httpx import AsyncClient
from kubernetes_asyncio.client import ApiClient
from redis.asyncio import BlockingConnectionPool, Redis
Expand Down Expand Up @@ -82,6 +83,9 @@ class ProcessContext:
config: Config
"""Gafaelfawr's configuration."""

firestore: firestore.AsyncClient | None
"""Client to talk to Firestore, if configured."""

http_client: AsyncClient
"""Shared HTTP client."""

Expand Down Expand Up @@ -126,6 +130,11 @@ async def from_config(cls, config: Config) -> Self:
ProcessContext
Shared context for a Gafaelfawr process.
"""
firestore_client = None
if config.firestore:
firestore_project = config.firestore.project
firestore_client = firestore.AsyncClient(project=firestore_project)

ldap_pool = None
if config.ldap:
client = LDAPClient(str(config.ldap.url))
Expand Down Expand Up @@ -161,6 +170,7 @@ async def from_config(cls, config: Config) -> Self:

return cls(
config=config,
firestore=firestore_client,
http_client=await http_client_dependency(),
ldap_pool=ldap_pool,
redis=redis_client,
Expand Down Expand Up @@ -352,9 +362,9 @@ def create_firestore_storage(self) -> FirestoreStorage:
FirestoreStorage
Newly-created Firestore storage.
"""
if not self._context.config.firestore:
if not self._context.firestore:
raise NotConfiguredError("Firestore is not configured")
return FirestoreStorage(self._context.config.firestore, self._logger)
return FirestoreStorage(self._context.firestore, self._logger)

def create_health_check_service(self) -> HealthCheckService:
"""Create a service for performing health checks.
Expand Down
39 changes: 21 additions & 18 deletions src/gafaelfawr/storage/firestore.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from google.cloud import firestore
from structlog.stdlib import BoundLogger

from ..config import FirestoreConfig
from ..constants import (
GID_MAX,
GID_MIN,
Expand Down Expand Up @@ -33,22 +32,26 @@ class FirestoreStorage:
"""Google Firestore storage layer.
Gafaelfawr supports assigning UIDs and GIDs from Google Firestore rather
than getting them from LDAP or upstream authentication tokens. This
module provides the read/write layer and transaction management for that
storage. It's used from inside a per-process cache.
than getting them from LDAP or upstream authentication tokens. This module
provides the read/write layer and transaction management for that
storage. It's used from inside a per-process cache.
This class authenticates to Google on creation, so it should not be
created fresh for every request.
Parameters
----------
config
Configuration for Google Firestore.
client
Firestore client to use.
logger
Logger for debug messages and errors.
"""

def __init__(self, config: FirestoreConfig, logger: BoundLogger) -> None:
self._config = config
def __init__(
self, client: firestore.AsyncClient, logger: BoundLogger
) -> None:
self._client = client
self._logger = logger
self._db = firestore.AsyncClient(project=config.project)

async def get_gid(self, group: str) -> int:
"""Get the GID for a group.
Expand All @@ -73,9 +76,9 @@ async def get_gid(self, group: str) -> int:
NoAvailableGidError
Raised if no more GIDs are available in that range.
"""
transaction = self._db.transaction()
group_ref = self._db.collection("groups").document(group)
counter_ref = self._db.collection("counters").document("gid")
transaction = self._client.transaction()
group_ref = self._client.collection("groups").document(group)
counter_ref = self._client.collection("counters").document("gid")
return await _get_or_assign_gid(
transaction,
group_name=group,
Expand Down Expand Up @@ -111,10 +114,10 @@ async def get_uid(self, username: str, *, bot: bool = False) -> int:
NoAvailableUidError
Raised if no more UIDs are available in that range.
"""
transaction = self._db.transaction()
user_ref = self._db.collection("users").document(username)
counter_name = "bot-uid" if bot else "uid"
counter_ref = self._db.collection("counters").document(counter_name)
transaction = self._client.transaction()
user_ref = self._client.collection("users").document(username)
counter = "bot-uid" if bot else "uid"
counter_ref = self._client.collection("counters").document(counter)
return await _get_or_assign_uid(
transaction,
username=username,
Expand All @@ -131,10 +134,10 @@ async def initialize(self) -> None:
silently do nothing.
"""
counter_refs = {
n: self._db.collection("counters").document(n)
n: self._client.collection("counters").document(n)
for n in _INITIAL_COUNTERS
}
transaction = self._db.transaction()
transaction = self._client.transaction()
await _initialize_in_transaction(
transaction, counter_refs, self._logger
)
Expand Down

0 comments on commit fa7896e

Please sign in to comment.