Skip to content
This repository was archived by the owner on Mar 26, 2026. It is now read-only.

Commit eebf51b

Browse files
committed
add documentation
1 parent 401320e commit eebf51b

File tree

3 files changed

+56
-17
lines changed

3 files changed

+56
-17
lines changed

gapic/cli/generate.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ def generate(request: typing.BinaryIO, output: typing.BinaryIO) -> None:
5757
[p.package for p in req.proto_file if p.name in req.file_to_generate]
5858
).rstrip(".")
5959

60+
# Create the generation cache context.
61+
# This provides the shared storage for the @cached_proto_context decorator.
62+
# 1. Performance: Memoizes `with_context` calls, speeding up generation significantly.
63+
# 2. Safety: The decorator uses this storage to "pin" Proto objects in memory.
64+
# This prevents Python's Garbage Collector from deleting objects created during
65+
# `API.build` while `Generator.get_response` is still using their IDs.
66+
# (See `gapic.utils.cache.cached_proto_context` for the specific pinning logic).
6067
with generation_cache_context():
6168
# Build the API model object.
6269
# This object is a frozen representation of the whole API, and is sent

gapic/utils/cache.py

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import functools
1616
import contextlib
1717
import threading
18-
from typing import Dict, Optional, Any
1918

2019

2120
def cached_property(fx):
@@ -48,46 +47,88 @@ def inner(self):
4847
return property(inner)
4948

5049

51-
# Thread-local storage for the simple cache dictionary
50+
# Thread-local storage for the simple cache dictionary.
51+
# This ensures that parallel generation tasks (if any) do not corrupt each other's cache.
5252
_thread_local = threading.local()
5353

5454

5555
@contextlib.contextmanager
5656
def generation_cache_context():
57-
"""Context manager to explicitly manage the cache lifecycle."""
58-
# Initialize the cache as a standard dictionary
57+
"""Context manager to explicitly manage the lifecycle of the generation cache.
58+
59+
This manager initializes a fresh dictionary in thread-local storage when entering
60+
the context and strictly deletes it when exiting.
61+
62+
**Memory Management:**
63+
The cache stores strong references to Proto objects to "pin" them in memory
64+
(see `cached_proto_context`). It is critical that this context manager deletes
65+
the dictionary in the `finally` block. Deleting the dictionary breaks the
66+
reference chain, allowing Python's Garbage Collector to finally free all the
67+
large Proto objects that were pinned during generation.
68+
"""
69+
# Initialize the cache as a standard dictionary.
5970
_thread_local.cache = {}
6071
try:
6172
yield
6273
finally:
63-
# Delete the dictionary to free all memory and pinned objects
74+
# Delete the dictionary to free all memory and pinned objects.
75+
# This is essential to prevent memory leaks in long-running processes.
6476
del _thread_local.cache
6577

6678

6779
def cached_proto_context(func):
68-
"""Decorator to memoize with_context calls based on self and collisions."""
80+
"""Decorator to memoize `with_context` calls based on object identity and collisions.
81+
82+
This mechanism provides a significant performance boost by preventing
83+
redundant recalculations of naming collisions during template rendering.
84+
85+
Since the Proto wrapper objects are unhashable (mutable), we use `id(self)` as
86+
the primary cache key. Normally, this is dangerous: if the object is garbage
87+
collected, Python might reuse its memory address for a *new* object, leading to
88+
a cache collision (the "Zombie ID" bug).
89+
90+
To prevent this, this decorator stores the value as a tuple: `(result, self)`.
91+
By keeping a reference to `self` in the cache value, we "pin" the object in
92+
memory. This forces the Garbage Collector to keep the object alive, guaranteeing
93+
that `id(self)` remains unique for the entire lifespan of the `generation_cache_context`.
94+
95+
Args:
96+
func (Callable): The function to decorate (usually `with_context`).
97+
98+
Returns:
99+
Callable: The wrapped function with caching and pinning logic.
100+
"""
69101

70102
@functools.wraps(func)
71103
def wrapper(self, *, collisions, **kwargs):
72-
# 1. Initialize cache if not provided (handles the root call case)
73104

105+
# 1. Check for active cache (returns None if context is not active)
74106
context_cache = getattr(_thread_local, "cache", None)
107+
108+
# If we are not inside a generation_cache_context (e.g. unit tests),
109+
# bypass the cache entirely.
75110
if context_cache is None:
76111
return func(self, collisions=collisions, **kwargs)
77112

78113
# 2. Create the cache key
114+
# We use frozenset for collisions to make it hashable.
115+
# We use id(self) because 'self' is not hashable.
79116
collisions_key = frozenset(collisions) if collisions else None
80117
key = (id(self), collisions_key)
81118

82119
# 3. Check Cache
83120
if key in context_cache:
121+
# The cache stores (result, pinned_object). We return just the result.
84122
return context_cache[key][0]
85123

86124
# 4. Execute the actual function
87125
# We ensure context_cache is passed down to the recursive calls
88126
result = func(self, collisions=collisions, **kwargs)
89127

90-
# 5. Update Cache
128+
# 5. Update Cache & Pin Object
129+
# We store (result, self). The reference to 'self' prevents garbage collection,
130+
# ensuring that 'id(self)' cannot be reused for a new object while this
131+
# cache entry exists.
91132
context_cache[key] = (result, self)
92133
return result
93134

tests/unit/utils/test_cache.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,6 @@ def bar(self):
3333
assert foo.call_count == 1
3434

3535

36-
# def test_generation_cache_context():
37-
# assert cache.generation_cache.get() is None
38-
# with cache.generation_cache_context():
39-
# assert isinstance(cache.generation_cache.get(), dict)
40-
# cache.generation_cache.get()["foo"] = "bar"
41-
# assert cache.generation_cache.get()["foo"] == "bar"
42-
# assert cache.generation_cache.get() is None
43-
44-
4536
def test_cached_proto_context():
4637
class Foo:
4738
def __init__(self):

0 commit comments

Comments
 (0)