Skip to content

Commit 9607f2e

Browse files
committed
Address PR review feedback (#11)
- Use one loop-state lookup\n- Preserve normalized key collisions
1 parent d350fec commit 9607f2e

2 files changed

Lines changed: 110 additions & 6 deletions

File tree

src/google/adk/plugins/bigquery_agent_analytics_plugin.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -548,23 +548,45 @@ def _normalize_json_native(
548548
out_dict: dict[str, Any] = {}
549549
replaced = False
550550
bad_keys = 0
551+
552+
def collision_safe_key(k: str) -> str:
553+
"""Allocates a unique key while preserving first-writer order."""
554+
nonlocal bad_keys, replaced
555+
if k not in out_dict:
556+
return k
557+
bad_keys += 1
558+
candidate = f"[KEY_COLLISION_{bad_keys}]{k}"
559+
while candidate in out_dict:
560+
bad_keys += 1
561+
candidate = f"[KEY_COLLISION_{bad_keys}]{k}"
562+
replaced = True
563+
return candidate
564+
551565
for k, v in obj.items():
552566
if budget[0] <= 0:
553-
out_dict["[SANITIZE_BUDGET_EXCEEDED]"] = "[SANITIZE_BUDGET_EXCEEDED]"
567+
budget_key = collision_safe_key("[SANITIZE_BUDGET_EXCEEDED]")
568+
out_dict[budget_key] = "[SANITIZE_BUDGET_EXCEEDED]"
554569
replaced = True
555570
break
571+
redact_value = False
556572
if isinstance(k, str):
557573
if type(k) is not str:
558574
k = str.__str__(k)
559575
k_lower = k.lower()
560576
if k_lower in _SENSITIVE_KEYS or k_lower.startswith("temp:"):
561-
budget[0] -= 1
562-
out_dict[k] = "[REDACTED]"
563-
continue
577+
redact_value = True
564578
else:
565579
bad_keys += 1
566580
k = f"[UNSUPPORTED_KEY_{bad_keys}]"
567581
replaced = True
582+
# Preserve every value after key normalization. The first writer
583+
# keeps the plain key; later colliders receive a marker that is
584+
# itself re-allocated around genuine marker-like user keys.
585+
k = collision_safe_key(k)
586+
if redact_value:
587+
budget[0] -= 1
588+
out_dict[k] = "[REDACTED]"
589+
continue
568590
norm_v, v_replaced = _normalize_json_native(
569591
v, max_len, depth + 1, budget
570592
)
@@ -3627,8 +3649,9 @@ async def _get_loop_state(
36273649
if self._generation != generation:
36283650
raise RuntimeError("BigQuery plugin is shutting down.")
36293651
self._cleanup_stale_loop_states()
3630-
if loop in self._loop_state_by_loop:
3631-
return self._loop_state_by_loop[loop]
3652+
state = self._loop_state_by_loop.get(loop)
3653+
if state is not None:
3654+
return state
36323655

36333656
# grpc.aio clients are loop-bound, so we create one per event loop.
36343657

tests/unittests/plugins/test_bigquery_agent_analytics_plugin.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10828,6 +10828,63 @@ def test_normalizer_redacts_and_bounds(self):
1082810828
assert out == preserved
1082910829
assert replaced is False
1083010830

10831+
def test_normalizer_preserves_post_normalization_key_collisions(self):
10832+
"""Normalized keys never silently overwrite an earlier value."""
10833+
normalize = bigquery_agent_analytics_plugin._normalize_json_native
10834+
10835+
unsupported_first, replaced = normalize(
10836+
{object(): "unsupported", "[UNSUPPORTED_KEY_1]": "genuine"},
10837+
10000,
10838+
)
10839+
assert unsupported_first == {
10840+
"[UNSUPPORTED_KEY_1]": "unsupported",
10841+
"[KEY_COLLISION_2][UNSUPPORTED_KEY_1]": "genuine",
10842+
}
10843+
assert replaced is True
10844+
10845+
genuine_first, replaced = normalize(
10846+
{"[UNSUPPORTED_KEY_1]": "genuine", object(): "unsupported"},
10847+
10000,
10848+
)
10849+
assert genuine_first == {
10850+
"[UNSUPPORTED_KEY_1]": "genuine",
10851+
"[KEY_COLLISION_2][UNSUPPORTED_KEY_1]": "unsupported",
10852+
}
10853+
assert replaced is True
10854+
10855+
marker_reserved, replaced = normalize(
10856+
{
10857+
object(): "unsupported",
10858+
"[KEY_COLLISION_2][UNSUPPORTED_KEY_1]": "reserved",
10859+
"[UNSUPPORTED_KEY_1]": "genuine",
10860+
},
10861+
10000,
10862+
)
10863+
assert marker_reserved == {
10864+
"[UNSUPPORTED_KEY_1]": "unsupported",
10865+
"[KEY_COLLISION_2][UNSUPPORTED_KEY_1]": "reserved",
10866+
"[KEY_COLLISION_3][UNSUPPORTED_KEY_1]": "genuine",
10867+
}
10868+
assert replaced is True
10869+
10870+
budget_reserved, replaced = normalize(
10871+
{
10872+
"[SANITIZE_BUDGET_EXCEEDED]": "reserved",
10873+
"[KEY_COLLISION_1][SANITIZE_BUDGET_EXCEEDED]": "marker",
10874+
"omitted": "value",
10875+
},
10876+
10000,
10877+
budget=[3],
10878+
)
10879+
assert budget_reserved == {
10880+
"[SANITIZE_BUDGET_EXCEEDED]": "reserved",
10881+
"[KEY_COLLISION_1][SANITIZE_BUDGET_EXCEEDED]": "marker",
10882+
"[KEY_COLLISION_2][SANITIZE_BUDGET_EXCEEDED]": (
10883+
"[SANITIZE_BUDGET_EXCEEDED]"
10884+
),
10885+
}
10886+
assert replaced is True
10887+
1083110888
@pytest.mark.asyncio
1083210889
async def test_native_secret_mapping_via_model_field_redacted(
1083310890
self,
@@ -12822,6 +12879,30 @@ async def blocking_shutdown(timeout=None):
1282212879
assert plugin._is_shutting_down is False
1282312880
assert loop not in plugin._loop_state_by_loop
1282412881

12882+
@pytest.mark.asyncio
12883+
async def test_get_loop_state_uses_single_lookup(
12884+
self, mock_auth_default, mock_bq_client
12885+
):
12886+
"""A concurrent removal cannot split an existence check from lookup."""
12887+
_ = mock_auth_default, mock_bq_client
12888+
plugin = bigquery_agent_analytics_plugin.BigQueryAgentAnalyticsPlugin(
12889+
PROJECT_ID, DATASET_ID, table_id=TABLE_ID
12890+
)
12891+
loop = asyncio.get_running_loop()
12892+
expected_state = mock.MagicMock()
12893+
12894+
class DeleteOnContainsDict(dict):
12895+
12896+
def __contains__(self, key):
12897+
present = super().__contains__(key)
12898+
if present:
12899+
del self[key]
12900+
return present
12901+
12902+
plugin._loop_state_by_loop = DeleteOnContainsDict({loop: expected_state})
12903+
12904+
assert await plugin._get_loop_state() is expected_state
12905+
1282512906
@pytest.mark.asyncio
1282612907
async def test_writer_built_during_shutdown_is_not_published(
1282712908
self, mock_auth_default, mock_bq_client

0 commit comments

Comments
 (0)