Skip to content

feat(bigtable): BigtableDataClientFactory session support - #13829

Merged
nimf merged 5 commits into
mainfrom
dataclientfactorysession
Jul 22, 2026
Merged

feat(bigtable): BigtableDataClientFactory session support#13829
nimf merged 5 commits into
mainfrom
dataclientfactorysession

Conversation

@nimf

@nimf nimf commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

This PR extends the session-based API path to support BigtableDataClientFactory.

Now the factory creates a single shared ChannelPool and ClientConfigurationManager once and hands lightweight child clients a reference to both.

The bulk of the implementation work is a rewrite of ChannelPoolDpImpl to correctly handle multiple tenants (different instances/app profiles) sharing a single channel pool.

ChannelPoolDpImpl: New Placement Model

Old design

The old pool organized channels into AfeChannelGroup objects — one deque of channels per observed AFE. When a channel's first session revealed its AFE (via onBeforeSessionStart), the channel was rehomed from the temporary startingGroup into the matching AfeChannelGroup, creating one if it didn't exist. New streams were then placed by picking the group with the fewest sessions.

This design had a problem for the multi-tenant factory case:
AFE affinity was tracked per channel, but for factory children different tenants on the same channel can be routed to different AFEs by RLS. A channel in group "AFE-7" might route tenant A to AFE-7 but tenant B somewhere else entirely.

New design

The new pool replaces the nested group structure with a flat channels list plus a routeObservations map keyed on (channelId, tenantKey) → lastObservedAfeId.

Data structures:
  • channels: List<ChannelWrapper> — all channels in ACTIVE or DRAINING state
  • routeObservations: Map<RouteKey, AfeId> — the last AFE seen for a given (channel, tenant) pair, populated on every onBeforeSessionStart and self-healing as RLS routes drift over time
  • sessionsPerAfeId: Multiset<AfeId> — global count of live sessions per AFE, used to find underloaded targets
  • TenantKey (new value type) — stamped onto CallOptions by TableBase so the pool can distinguish tenants within the same shared pool
Placement (newStream):
  1. Capacity mode — find the AFE with the smallest session count that is still below softMaxPerGroup:
    - 3a — Preferred: pick the ACTIVE channel whose last observed route for this tenant was that AFE, choosing the least-loaded among candidates. This is the steady-state path: RLS is stable per (channel, tenant), so repeat sessions for the same tenant land on the same AFE.
    - 3b — Unobserved-tenant fallback: if no route observation exists for this tenant on any channel, pick any ACTIVE channel with remaining capacity (least-loaded). The actual AFE is unknown until onBeforeSessionStart fires; routeObservations is updated then for future placements.
  2. Diversity mode — entered when all known AFEs are at cap, or the pool is empty: prefer an ACTIVE channel with fewer than softMaxPerGroup / 2 outstanding streams. If none exists, open a new channel and absorb whichever AFE it lands on.
Scale-in (DRAINING state):

The old "thinning" logic would call channel.shutdown() during serviceChannels(). The new approach introduces a two-state channel lifecycle (ACTIVE / DRAINING). When serviceChannels() decides to shrink the pool it marks excess channels DRAINING — they stop accepting new streams but continue serving existing ones. A DRAINING channel is removed only when its numOutstanding reaches zero (in releaseChannel). Drain candidates are chosen by pickDrainCandidates, which prefers idle channels first, then least-recently-used, then oldest.

Route observation cleanup:

removeChannel purges all routeObservations entries for the removed channel's ID, preventing the map from growing unboundedly as channels cycle.

Other changes

BigtableDataClientFactory / BigtableClientContext:

  • BigtableDataClientFactory.create() now calls BigtableClientContext.createForFactory() (a new entry point) instead of manually disabling sessions. The factory context initializes a shared ShimImpl (channel pool + config manager) that is reused by all children.
  • BigtableClientContext.createChild() now creates a lightweight ShimImpl child via ShimImpl.createForFactoryChild() rather than sharing a DisabledShim.

ShimImpl / Client:

  • Client.channelPool changed from a bare ChannelPool reference to Resource<ChannelPool> so the factory-child constructor can hold a shared (non-closing) reference and the standard constructor holds an owned one.
  • ShimImpl.configManager similarly wrapped in Resource<> so close() is a no-op for factory children (the parent owns the manager's lifecycle).

@nimf
nimf requested review from a team as code owners July 19, 2026 23:23
@nimf
nimf requested a review from mutianf July 19, 2026 23:23

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the Bigtable client and channel pool implementation to support multi-tenant factory usage and dynamic channel pool sizing. It introduces tenant-aware routing via ChannelPoolOptions and TenantKey, and updates ChannelPoolDpImpl to manage channel lifecycles using ACTIVE and DRAINING states. The review feedback highlights a compilation error in ChannelPoolDpImpl.java due to a missing import for Collectors, and suggests an optimization in diversity mode to prevent excessive channel creation under burst loads. Additionally, a thread-safety issue was identified in dumpState(), where unsynchronized access to guarded fields could cause concurrent modification exceptions, with a recommendation to use explicit locks instead of the synchronized keyword.

This commit extends the session-based API path to support BigtableDataClientFactory.

Now the factory creates a single shared ChannelPool and
  ClientConfigurationManager once and hands lightweight child clients a reference to both.

  The bulk of the implementation work is a rewrite of ChannelPoolDpImpl to correctly handle multiple tenants (different instances/app profiles) sharing a single channel pool.

  ---
  ChannelPoolDpImpl: New Placement Model

  Old design

  The old pool organized channels into AfeChannelGroup objects — one deque of channels per observed AFE. When a channel's first session revealed its AFE (via onBeforeSessionStart), the channel was rehomed from
  the temporary startingGroup into the matching AfeChannelGroup, creating one if it didn't exist. New streams were then placed by picking the group with the fewest sessions.

  This design had a problem for the multi-tenant factory case:
  AFE affinity was tracked per channel, but for factory children different tenants on the same channel can be routed to different AFEs by RLS. A channel in group "AFE-7" might route tenant A to AFE-7 but
  tenant B somewhere else entirely.

  New design

  The new pool replaces the nested group structure with a flat channels list plus a routeObservations map keyed on (channelId, tenantKey) → lastObservedAfeId.

  Data structures:
  - channels: List<ChannelWrapper> — all channels in ACTIVE or DRAINING state
  - routeObservations: Map<RouteKey, AfeId> — the last AFE seen for a given (channel, tenant) pair, populated on every onBeforeSessionStart and self-healing as RLS routes drift over time
  - sessionsPerAfeId: Multiset<AfeId> — global count of live sessions per AFE, used to find underloaded targets
  - TenantKey (new value type) — stamped onto CallOptions by TableBase so the pool can distinguish tenants within the same shared pool

  Placement (newStream):

  1. Capacity mode — find the AFE with the smallest session count that is still below softMaxPerGroup:
    - 3a — Preferred: pick the ACTIVE channel whose last observed route for this tenant was that AFE, choosing the least-loaded among candidates. This is the steady-state path: RLS is stable per (channel,
  tenant), so repeat sessions for the same tenant land on the same AFE.
    - 3b — Unobserved-tenant fallback: if no route observation exists for this tenant on any channel, pick any ACTIVE channel with remaining capacity (least-loaded). The actual AFE is unknown until
  onBeforeSessionStart fires; routeObservations is updated then for future placements.
  2. Diversity mode — entered when all known AFEs are at cap, or the pool is empty: prefer an ACTIVE channel with fewer than softMaxPerGroup / 2 outstanding streams. If none exists, open a new channel and
  absorb whichever AFE it lands on.

  Scale-in (DRAINING state):

  The old "thinning" logic would call channel.shutdown() during serviceChannels(). The new approach introduces a two-state channel lifecycle
  (ACTIVE / DRAINING). When serviceChannels() decides to shrink the pool it marks excess channels DRAINING — they stop accepting new streams but continue serving existing ones. A DRAINING channel is removed
  only when its numOutstanding reaches zero (in releaseChannel). Drain candidates are chosen by pickDrainCandidates, which prefers idle channels first, then least-recently-used, then oldest.

  Route observation cleanup:

  removeChannel purges all routeObservations entries for the removed channel's ID, preventing the map from growing unboundedly as channels cycle.

  ---
  Other changes

  BigtableDataClientFactory / BigtableClientContext:
  - BigtableDataClientFactory.create() now calls BigtableClientContext.createForFactory() (a new entry point) instead of manually disabling sessions. The factory context initializes a shared ShimImpl (channel
  pool + config manager) that is reused by all children.
  - BigtableClientContext.createChild() now creates a lightweight ShimImpl child via ShimImpl.createForFactoryChild() rather than sharing a DisabledShim.

  ShimImpl / Client:
  - Client.channelPool changed from a bare ChannelPool reference to Resource<ChannelPool> so the factory-child constructor can hold a shared (non-closing) reference and the standard constructor holds an owned
  one.
  - ShimImpl.configManager similarly wrapped in Resource<> so close() is a no-op for factory children (the parent owns the manager's lifecycle).
@nimf
nimf force-pushed the dataclientfactorysession branch from 78841fa to 90f95ef Compare July 19, 2026 23:28
@nimf nimf changed the title Rewrite ChannelPoolDpImpl to support BigtableDataClientFactory feat(bigtable): BigtableDataClientFactory session support Jul 19, 2026

/**
* Factory-child constructor. Uses a pre-built, shared {@link ChannelPool} and {@link
* ClientConfigurationManager}. The pool is already started and must not be closed by this client.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe reword "The pool is already started and must not be closed by this client." to something like "The pool should be created with Resource.createShared() and closed when the factory closes"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two public Client(...) constructors are distinguished only by the final parameter (ChannelProvider vs Resource). This is fragile — a factory (a static factory method or clearly named builders) would be safer than type-only overload resolution, especially since one throws IOException and the other doesn't.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I can wrap it in static factory, but looks like we'll still need two constructors like these. Leaving it as is for now.


public TenantKey(@Nullable InstanceName instanceName, String appProfileId) {
this.instanceName = instanceName;
this.appProfileId = appProfileId == null ? "" : appProfileId;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: is this check necessary? I think we can just do this.appProfileId = appProfileId

@nimf
nimf enabled auto-merge (squash) July 22, 2026 16:15
@nimf
nimf merged commit 284ce13 into main Jul 22, 2026
211 of 214 checks passed
@nimf
nimf deleted the dataclientfactorysession branch July 22, 2026 16:32
mutianf pushed a commit to mutianf/google-cloud-java that referenced this pull request Jul 24, 2026
…#13829)

This PR extends the session-based API path to support
`BigtableDataClientFactory`.

Now the factory creates a single shared `ChannelPool` and
`ClientConfigurationManager` once and hands lightweight child clients a
reference to both.

The bulk of the implementation work is a rewrite of `ChannelPoolDpImpl`
to correctly handle multiple tenants (different instances/app profiles)
sharing a single channel pool.

### `ChannelPoolDpImpl`: New Placement Model

#### Old design

The old pool organized channels into `AfeChannelGroup` objects — one
deque of channels per observed AFE. When a channel's first session
revealed its AFE (via `onBeforeSessionStart`), the channel was rehomed
from the temporary `startingGroup` into the matching `AfeChannelGroup`,
creating one if it didn't exist. New streams were then placed by picking
the group with the fewest sessions.

  This design had a problem for the multi-tenant factory case:
AFE affinity was tracked per channel, but for factory children different
tenants on the same channel can be routed to different AFEs by RLS. A
channel in group "AFE-7" might route tenant A to AFE-7 but tenant B
somewhere else entirely.

#### New design

The new pool replaces the nested group structure with a flat `channels`
list plus a `routeObservations` map keyed on `(channelId, tenantKey) →
lastObservedAfeId`.

##### Data structures:
- `channels: List<ChannelWrapper>` — all channels in ACTIVE or DRAINING
state
- `routeObservations: Map<RouteKey, AfeId>` — the last AFE seen for a
given (channel, tenant) pair, populated on every `onBeforeSessionStart`
and self-healing as RLS routes drift over time
- `sessionsPerAfeId: Multiset<AfeId>` — global count of live sessions
per AFE, used to find underloaded targets
- `TenantKey` (new value type) — stamped onto `CallOptions` by
`TableBase` so the pool can distinguish tenants within the same shared
pool

##### Placement (`newStream`):

1. Capacity mode — find the AFE with the smallest session count that is
still below `softMaxPerGroup`:
- 3a — Preferred: pick the ACTIVE channel whose last observed route for
this tenant was that AFE, choosing the least-loaded among candidates.
This is the steady-state path: RLS is stable per (channel, tenant), so
repeat sessions for the same tenant land on the same AFE.
- 3b — Unobserved-tenant fallback: if no route observation exists for
this tenant on any channel, pick any ACTIVE channel with remaining
capacity (least-loaded). The actual AFE is unknown until
`onBeforeSessionStart` fires; `routeObservations` is updated then for
future placements.
2. Diversity mode — entered when all known AFEs are at cap, or the pool
is empty: prefer an ACTIVE channel with fewer than `softMaxPerGroup / 2`
outstanding streams. If none exists, open a new channel and absorb
whichever AFE it lands on.

##### Scale-in (DRAINING state):

The old "thinning" logic would call `channel.shutdown()` during
serviceChannels(). The new approach introduces a two-state channel
lifecycle (`ACTIVE` / `DRAINING`). When `serviceChannels()` decides to
shrink the pool it marks excess channels DRAINING — they stop accepting
new streams but continue serving existing ones. A DRAINING channel is
removed only when its `numOutstanding` reaches zero (in
`releaseChannel`). Drain candidates are chosen by `pickDrainCandidates`,
which prefers idle channels first, then least-recently-used, then
oldest.

##### Route observation cleanup:

`removeChannel` purges all `routeObservations` entries for the removed
channel's ID, preventing the map from growing unboundedly as channels
cycle.

### Other changes

#### `BigtableDataClientFactory` / `BigtableClientContext`:
- `BigtableDataClientFactory.create()` now calls
`BigtableClientContext.createForFactory()` (a new entry point) instead
of manually disabling sessions. The factory context initializes a shared
`ShimImpl` (channel pool + config manager) that is reused by all
children.
- `BigtableClientContext.createChild()` now creates a lightweight
`ShimImpl` child via `ShimImpl.createForFactoryChild()` rather than
sharing a `DisabledShim`.

#### `ShimImpl` / `Client`:
- `Client.channelPool` changed from a bare `ChannelPool` reference to
`Resource<ChannelPool>` so the factory-child constructor can hold a
shared (non-closing) reference and the standard constructor holds an
owned one.
- `ShimImpl.configManager` similarly wrapped in `Resource<>` so
`close()` is a no-op for factory children (the parent owns the manager's
lifecycle).
mutianf pushed a commit to mutianf/google-cloud-java that referenced this pull request Jul 24, 2026
…#13829)

This PR extends the session-based API path to support
`BigtableDataClientFactory`.

Now the factory creates a single shared `ChannelPool` and
`ClientConfigurationManager` once and hands lightweight child clients a
reference to both.

The bulk of the implementation work is a rewrite of `ChannelPoolDpImpl`
to correctly handle multiple tenants (different instances/app profiles)
sharing a single channel pool.

### `ChannelPoolDpImpl`: New Placement Model

#### Old design

The old pool organized channels into `AfeChannelGroup` objects — one
deque of channels per observed AFE. When a channel's first session
revealed its AFE (via `onBeforeSessionStart`), the channel was rehomed
from the temporary `startingGroup` into the matching `AfeChannelGroup`,
creating one if it didn't exist. New streams were then placed by picking
the group with the fewest sessions.

  This design had a problem for the multi-tenant factory case:
AFE affinity was tracked per channel, but for factory children different
tenants on the same channel can be routed to different AFEs by RLS. A
channel in group "AFE-7" might route tenant A to AFE-7 but tenant B
somewhere else entirely.

#### New design

The new pool replaces the nested group structure with a flat `channels`
list plus a `routeObservations` map keyed on `(channelId, tenantKey) →
lastObservedAfeId`.

##### Data structures:
- `channels: List<ChannelWrapper>` — all channels in ACTIVE or DRAINING
state
- `routeObservations: Map<RouteKey, AfeId>` — the last AFE seen for a
given (channel, tenant) pair, populated on every `onBeforeSessionStart`
and self-healing as RLS routes drift over time
- `sessionsPerAfeId: Multiset<AfeId>` — global count of live sessions
per AFE, used to find underloaded targets
- `TenantKey` (new value type) — stamped onto `CallOptions` by
`TableBase` so the pool can distinguish tenants within the same shared
pool

##### Placement (`newStream`):

1. Capacity mode — find the AFE with the smallest session count that is
still below `softMaxPerGroup`:
- 3a — Preferred: pick the ACTIVE channel whose last observed route for
this tenant was that AFE, choosing the least-loaded among candidates.
This is the steady-state path: RLS is stable per (channel, tenant), so
repeat sessions for the same tenant land on the same AFE.
- 3b — Unobserved-tenant fallback: if no route observation exists for
this tenant on any channel, pick any ACTIVE channel with remaining
capacity (least-loaded). The actual AFE is unknown until
`onBeforeSessionStart` fires; `routeObservations` is updated then for
future placements.
2. Diversity mode — entered when all known AFEs are at cap, or the pool
is empty: prefer an ACTIVE channel with fewer than `softMaxPerGroup / 2`
outstanding streams. If none exists, open a new channel and absorb
whichever AFE it lands on.

##### Scale-in (DRAINING state):

The old "thinning" logic would call `channel.shutdown()` during
serviceChannels(). The new approach introduces a two-state channel
lifecycle (`ACTIVE` / `DRAINING`). When `serviceChannels()` decides to
shrink the pool it marks excess channels DRAINING — they stop accepting
new streams but continue serving existing ones. A DRAINING channel is
removed only when its `numOutstanding` reaches zero (in
`releaseChannel`). Drain candidates are chosen by `pickDrainCandidates`,
which prefers idle channels first, then least-recently-used, then
oldest.

##### Route observation cleanup:

`removeChannel` purges all `routeObservations` entries for the removed
channel's ID, preventing the map from growing unboundedly as channels
cycle.

### Other changes

#### `BigtableDataClientFactory` / `BigtableClientContext`:
- `BigtableDataClientFactory.create()` now calls
`BigtableClientContext.createForFactory()` (a new entry point) instead
of manually disabling sessions. The factory context initializes a shared
`ShimImpl` (channel pool + config manager) that is reused by all
children.
- `BigtableClientContext.createChild()` now creates a lightweight
`ShimImpl` child via `ShimImpl.createForFactoryChild()` rather than
sharing a `DisabledShim`.

#### `ShimImpl` / `Client`:
- `Client.channelPool` changed from a bare `ChannelPool` reference to
`Resource<ChannelPool>` so the factory-child constructor can hold a
shared (non-closing) reference and the standard constructor holds an
owned one.
- `ShimImpl.configManager` similarly wrapped in `Resource<>` so
`close()` is a no-op for factory children (the parent owns the manager's
lifecycle).
whowes pushed a commit that referenced this pull request Jul 30, 2026
🤖 I have created a release *beep* *boop*
---


<details><summary>1.89.0</summary>

##
[1.89.0](v1.88.0...v1.89.0)
(2026-07-29)


### Features

* **agentidentity:** onboard v1 and v1beta API versions
([#13796](#13796))
([1b1300d](1b1300d))
* **auth:** add JSpecify Null annotations to Auth
([#13842](#13842))
([f6f24d4](f6f24d4))
* **bigquery-jdbc:** add `SSLTrustStoreType` and `SSLTrustStoreProvider`
connection properties
([#13858](#13858))
([9449be1](9449be1))
* **bigquery-jdbc:** add otel trace and span IDs to local logs
([#13935](#13935))
([2801fbd](2801fbd))
* **bigquery-jdbc:** implement BigQueryParameterMetaData and dynamic
type mappings
([#13812](#13812))
([3d67dba](3d67dba))
* **bigquery-jdbc:** implement parameter setters in PreparedStatement
([#13792](#13792))
([94f7404](94f7404))
* **bigquery-jdbc:** Migrate `getImportedKeys` and `getCrossReference`
to BQ API
([#13692](#13692))
([082b046](082b046))
* **bigquery-jdbc:** migrate `getPrimaryKeys` to use BQ API
([#13691](#13691))
([1951f49](1951f49))
* **bigquery-jdbc:** OpenTelemetry integration in BQ JDBC
([#12902](#12902))
([af18f65](af18f65))
* **bigquery-jdbc:** optimize memory footprint for JSON result set
streaming
([#13660](#13660))
([11f26d3](11f26d3))
* **bigquery-jdbc:** standardize parameter handling and calendar
defensive copying across statement interfaces
([#13805](#13805))
([ccd13eb](ccd13eb))
* **bigtable:** add view_parameters support to BoundStatement
([#13673](#13673))
([d5cc437](d5cc437))
* **bigtable:** BigtableDataClientFactory session support
([#13829](#13829))
([284ce13](284ce13))
* **commerceproducer:** onboard v1beta API
([#13814](#13814))
([61b89b3](61b89b3))
* Default to least-in-flight balancing for Bigtable unary clients
([#13802](#13802))
([ac9ccd1](ac9ccd1))
* **firestore:** Add support for 16MB documents
([#13478](#13478))
([1b7c2e0](1b7c2e0))
* **gapic-generator:** add JSpecify Null annotations to the generator
classes
([#13769](#13769))
([843bd7c](843bd7c))
* **gapic-generator:** Add Nullable annotation to generated classes
([#13558](#13558))
([e3e9d0b](e3e9d0b))
* **gapic-generator:** Add NullMarked annotation to generated classes
([#13584](#13584))
([b7a8504](b7a8504))
* **gax-httpjson:** Add Post Quantum Cryptography (PQC) Support by
default via Conscrypt
([#13853](#13853))
([550df81](550df81))
* **gax-java:** add JSpecify Null annotations to gax
([#13799](#13799))
([65aee08](65aee08))
* **google/cloud/sql:** onboard a new library
([#13864](#13864))
([38e272e](38e272e))
* **google/maps/navconnect/v1:** onboard a new library
([#13927](#13927))
([2256394](2256394))
* **maps-isochrones:** onboard v1 API
([#13817](#13817))
([3037ab3](3037ab3))
* port secure_context testing support to executor proxy
([#13522](#13522))
([0f81bf0](0f81bf0))
* **productregistry:** onboard v1 API
([#13816](#13816))
([9517313](9517313))
* **storage:** allow checksum on appendable upload finalization
([#13833](#13833))
([ddf9add](ddf9add))
* **storage:** enable App-Centric Observability (ACO) support in Otel
([#13248](#13248))
([4329896](4329896))


### Bug Fixes

* **bigquery-jdbc:** Add PerConnectionHandler to list of excempted
logging classes
([#13888](#13888))
([50b3c24](50b3c24))
* **bigquery-jdbc:** add preferIPv4Stack to argLine for Kokoro
reliability
([#13923](#13923))
([d5e33d6](d5e33d6))
* **bigquery-jdbc:** add service resource transformer for standalone IT
([#13893](#13893))
([dc80fe8](dc80fe8))
* **bigquery-jdbc:** align metadata methods error handling with spec
([#13793](#13793))
([d85fb10](d85fb10))
* **bigquery-jdbc:** fix WriteAPI when running in restricted environment
([#13856](#13856))
([66ba925](66ba925))
* **bigquery-jdbc:** refine temporal timezone coercion and
PreparedStatement parameter setters
([#13813](#13813))
([6f68c4d](6f68c4d))
* **bigquery-jdbc:** resolve `ITOpenTelemetryTest` pipeline and trace
validation failures
([#13898](#13898))
([c18141d](c18141d))
* **bigquery-jdbc:** resolve failing otel IT in nightly
([#13915](#13915))
([ac79713](ac79713))
* **bigquery:** resultSet.getLong() does not truncate for large int64
values
([#13718](#13718))
([bc19822](bc19822))
* **bigquery:** support optional fields in BigLakeConfiguration to
prevent NPE on Iceberg/Lakehouse tables
([#13733](#13733))
([e2cca4d](e2cca4d))
* **bigtable:** add materialized view routing param to ReadRows and Sa…
([#13918](#13918))
([4ddf250](4ddf250))
* **bigtable:** bound SessionPoolImpl lock to prevent pod-wide wedge
([#13890](#13890))
([ed87a68](ed87a68))
* **bigtable:** fix session creation leaks
([#13887](#13887))
([d586d07](d586d07))
* **bigtable:** prevent ClientConfigurationManagerTest from wedging on…
([#13907](#13907))
([725086d](725086d))
* **bigtable:** stop installing DirectpathEnforcer on the directpath
pool
([#13880](#13880))
([5f2e056](5f2e056))
* **bom:** make release-note-generation Java 8 compatible
([#13837](#13837))
([bc18390](bc18390))
* **ci:** fix java-cloud-bom release-notes workflow errors
([#13682](#13682))
([b679835](b679835))
* deprecate resource detector
([#13844](#13844))
([aba4f01](aba4f01))
* **deps:** align logback versions and add java8 profile in storage
([#13678](#13678))
([7e57092](7e57092))
* do not start stream with direct executor
([#13945](#13945))
([630e790](630e790))
* fix java-cloud-bom README update workflow after monorepo migration
([#13892](#13892))
([5b8e295](5b8e295))
* **oauth2_http:** Avoid retrying on 4xx errors during GCE metadata ping
([#13715](#13715))
([537c16c](537c16c))
* regenerate
([#13714](#13714))
([8a72860](8a72860))
* regenerate libraries
([#13703](#13703))
([a29ea79](a29ea79)),
refs
[#13690](#13690)
* **release:** handle missing release tags gracefully in
release-note-generation
([#13795](#13795))
([43005fb](43005fb))
* **release:** resolve first-party-dependencies SNAPSHOT in
libraries-bom
([#13790](#13790))
([d5bfe21](d5bfe21))
* **spanner:** avoid data race on DIRECTPATH_CHANNEL_CREATED by using
volatile
([#13727](#13727))
([1e05ea5](1e05ea5))
* **spanner:** prevent fastpath tablet routing flaps
([#13803](#13803))
([4dabdac](4dabdac))
* **storage:** BidiAppendableUpload Takeover operation fixes
([#13776](#13776))
([f2d0474](f2d0474))
* **storage:** correctly insert explicit nulls for json patch updates
([#13716](#13716))
([4fb3f4b](4fb3f4b))
* update group id mapping
([#13698](#13698))
([50a72e1](50a72e1))
* use a new managed channel builder when creating channels
([#13684](#13684))
([a049999](a049999))


### Performance Improvements

* **bigquery-jdbc:** optimize getExportedKeys performance using hybrid
metadata lookup
([#13734](#13734))
([c9738ea](c9738ea))


### Dependencies

* Add Conscrypt to shared-deps
([#13838](#13838))
([05ce6ce](05ce6ce))
* move conscrypt from third-party-dependencies POM to gax-java POM
([#13948](#13948))
([1e634ec](1e634ec))
* **shared-deps:** migrate awaitility to shared-dependencies
([#13671](#13671))
([abb91ef](abb91ef))
* **shared-deps:** switch conscrypt shared dependency to
conscrypt-openjdk-uber
([#13845](#13845))
([2e3f208](2e3f208))
* Update gRPC-Java to v1.82.2
([#13877](#13877))
([da228d8](da228d8))
* Update http-client to v2.2.0
([#13854](#13854))
([334a2c5](334a2c5))
* Update Protobuf-Java to v4.33.6
([#13876](#13876))
([5c6478c](5c6478c))
* Upgrade Guava to v33.6.0-jre
([#13875](#13875))
([41a7a52](41a7a52))


### Documentation

* add ErrorProne and NullAway integration guide and JSpecify migration
playbook
([#13882](#13882))
([d5ea739](d5ea739))
* add JSpecify nullness guidelines to AGENTS.md
([#13881](#13881))
([54b846b](54b846b))
</details>

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com>
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.

2 participants