Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
47 changes: 46 additions & 1 deletion fe/fe-connector/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,51 @@ Three design rules shape everything in this directory:
reached only through the SPI, and optional behavior is opt-in via
capabilities (see "Reading the API").

## Catalog cache ownership

`ExternalCatalog` owns one `CatalogMetaCache` root. Each connector instance acquires a domain through
`ConnectorContext.createCacheDomain`; embedded siblings acquire child domains. FE entries and connector
entries share the root's registry and catalog/global budgets. Domains qualify physical registration names,
wrap plugin callbacks with the registering classloader, and retire only their own registrations. Closing an
old instance never invalidates a replacement instance's resource scope.

Resource dependencies are independent of domain lifetime:

```
Catalog root
LOCAL -> database -> table -> schema/content
REMOTE -> database -> table -> partition
ID -> database id -> table id -> derived row count
```

Each cache value declares one most-specific `ScopePath`. Membership snapshots have typed collection scopes.
`CatalogMetadataChanges` projects a semantic refresh, mutation, DDL, event or replay into these coordinates
and detaches all affected scopes at one publication barrier. Content changes preserve lookup objects and
name lists; explicit lookup refresh also invalidates the target identity and parent membership. Newly
written journals persist local/remote identities and the operation's lookup flag. Old records widen only
the remote scope when a cache-only lookup cannot recover the remote identity.

A definitive absent base-table handle can repair this FE's stale lookup state through the same dependency
planner, without writing a DDL journal. Before the remote read, FE observes both the exact retained table
and its matching names snapshot. Checking those observations and detaching dependencies share one publication
write phase. Replacement, name refresh, content invalidation or logical registration retirement makes the
old observation ineligible, so a delayed negative result cannot clear a recreated table. Cold names,
system/session-bypassed reads and remote failures do not authorize shared-cache repair. Connector handle
lookups must return absence only for a missing table and propagate other failures.

The root captures an immutable resource normalization policy. With case-insensitive table lookup (mode 2),
LOCAL table scopes fold case while cache keys and displayed names retain their original spelling; REMOTE
scopes retain the connector's case semantics. Registration, bulk-load parents and invalidation use the same
projection, so an old spelling cannot publish after its case-equivalent name is invalidated. A mode 2 lookup invalidation (including rename and its event records)
also fences the database's numeric derived state: IDs published after scope planning must not leave a stale
row count behind. Sibling objects and schemas remain cached, while row counts in that database are reloaded.
Changing table-name mode retires the root rather than changing the policy of live registrations.

Gateways with different physical identities implement the pure `Connector.cacheDependencies` projection;
it must not perform I/O or construct siblings. Partition names are translated by `partitionCacheScopes`.
The host applies the resulting scopes; gateways do not maintain a second list of invalidation callbacks.
Immutable path-addressed caches can stay at catalog scope and survive table refreshes.

## Module Map

Roles only — one line each. For anything deeper, read the module's javadoc
Expand Down Expand Up @@ -61,7 +106,7 @@ endpoint properties)

| Module | Role |
|---|---|
| `fe-connector-cache` | Self-contained caching framework used by several connectors. No fe-core dependency; it is bundled into each consuming plugin, so shared third-party libraries stay at the consumers' lowest common version (see the version notes in consumer poms). |
| `fe-connector-cache` | Shared metadata-cache framework with no fe-core dependency. The parent-first `org.apache.doris.connector.*` loader policy gives FE and plugins one runtime identity; plugin packaging does not create private cache owners. ConnectorContext supplies instance domains under the catalog root. |
| `fe-connector-hms-hive-shade` | Slim, relocated HMS metastore-client closure for connectors that speak HMS thrift. The pom comments say exactly what relocates where and why. |
| `fe-connector-paimon-hive-shade` | Paimon-private relocated HMS-thrift closure; same idea, different owner. |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public AdbcConnector(Map<String, String> properties, ConnectorContext context) {
// The raw map, because the cache knobs are the shared framework's keys rather than this
// connector's: CacheSpec owns their names, and mirroring them as fields here would give the
// validator and the reader two things to drift apart.
this.metaCache = CatalogMetaCache.managed(context.getCatalogId(), "adbc", props.getRaw());
this.metaCache = context.createCacheDomain("adbc", props.getRaw());
try {
this.metadataCache = new AdbcMetadataCache(metaCache, props.getRaw());
} catch (RuntimeException | Error e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,14 @@ public AdbcMetadataCache(Map<String, String> properties) {
this.owner = Objects.requireNonNull(owner, "owner can not be null");
CacheSpec spec = cacheSpec(properties);
this.namespaces = owner.create(MetaCacheDefinition
.<String, List<AdbcNamespace>>builder("adbc-namespaces", spec, ignored -> ScopePath.catalog())
.<String, List<AdbcNamespace>>builder("adbc-namespaces", spec,
ignored -> ScopePath.databaseCollection())
.budgetGroup(ENTRY)
.sizeEstimator(MetaCacheSizeEstimators.reflective())
.build());
this.tableNames = owner.create(MetaCacheDefinition
.<TableNameKey, List<String>>builder("adbc-table-names", spec,
key -> ScopePath.database(key.dorisDbName))
key -> ScopePath.tableCollection(key.dorisDbName))
.budgetGroup(ENTRY)
.sizeEstimator(MetaCacheSizeEstimators.reflective())
.build());
Expand Down Expand Up @@ -174,11 +175,7 @@ Schema tableSchema(AdbcTableHandle handle, Supplier<Schema> loader) {
* new name in, and the one the user tried would appear to do nothing.
*/
void invalidateTable(String dbName, String tableName) {
owner.invalidateTable(dbName, tableName);
// A table refresh must also forget the parent name listing, so a remotely-created table can be found.
// This is an ancestor materialization, not a sibling-cache dependency, and therefore cannot be selected
// by the table's descendant scope.
tableNames.invalidateKey(TableNameKey.forDatabase(dbName));
owner.invalidateTableAndNames(dbName, tableName);
}

/** {@code REFRESH DATABASE}: that database's table listing and every schema in it. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.apache.doris.connector.cache.CacheSpec;
import org.apache.doris.connector.cache.CatalogMetaCache;
import org.apache.doris.connector.cache.MetaCacheGovernance;
import org.apache.doris.connector.cache.ScopePath;

import org.apache.arrow.vector.types.pojo.ArrowType;
import org.apache.arrow.vector.types.pojo.Field;
Expand All @@ -41,6 +42,45 @@
*/
class AdbcMetadataCacheTest {

@Test
void rootResourceInvalidationIncludesRememberedMemberships() {
try (CatalogMetaCache root = CatalogMetaCache.unmanaged()) {
CatalogMetaCache domain = root.openDomain("adbc", ScopePath.Coordinates.REMOTE,
getClass().getClassLoader());
AdbcMetadataCache cache = new AdbcMetadataCache(domain, Map.of());
Counting<List<AdbcNamespace>> namespaces = new Counting<>(List.of(MAIN, OTHER));
Counting<List<String>> main = new Counting<>(List.of("t1"));
Counting<List<String>> other = new Counting<>(List.of("t9"));
Counting<Schema> schema = new Counting<>(SCHEMA);
cache.namespaces(namespaces);
cache.tableNames(MAIN, main);
cache.tableNames(OTHER, other);
cache.tableSchema(MAIN_T1, schema);

root.invalidateScopes(List.of(
ScopePath.table("main", "t1").in(ScopePath.Coordinates.REMOTE),
ScopePath.tableCollection("main").in(ScopePath.Coordinates.REMOTE)));
cache.namespaces(namespaces);
cache.tableNames(MAIN, main);
cache.tableNames(OTHER, other);
cache.tableSchema(MAIN_T1, schema);
Assertions.assertEquals(1, namespaces.calls);
Assertions.assertEquals(2, main.calls);
Assertions.assertEquals(1, other.calls);
Assertions.assertEquals(2, schema.calls);

root.invalidateScopes(List.of(
ScopePath.database("main").in(ScopePath.Coordinates.REMOTE),
ScopePath.databaseCollection().in(ScopePath.Coordinates.REMOTE)));
cache.namespaces(namespaces);
cache.tableNames(MAIN, main);
cache.tableNames(OTHER, other);
Assertions.assertEquals(2, namespaces.calls);
Assertions.assertEquals(3, main.calls);
Assertions.assertEquals(1, other.calls);
}
}

@Test
void managedCachesAccountAllPayloadsAndReleaseReservations() {
long baseline = MetaCacheGovernance.globalEstimatedWeight();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package org.apache.doris.connector.cache;

import java.util.function.Supplier;

/** Restores the registering plugin's loader around callbacks, including asynchronous cleanup. */
public final class CacheCallbackContext {
private final ClassLoader loader;

public CacheCallbackContext(ClassLoader loader) {
this.loader = loader;
}

public <T> T call(Supplier<T> callback) {
Thread thread = Thread.currentThread();
ClassLoader previous = thread.getContextClassLoader();
if (previous == loader) {
return callback.get();
}
try {
thread.setContextClassLoader(loader);
return callback.get();
} finally {
thread.setContextClassLoader(previous);
}
}

public void run(Runnable callback) {
call(() -> {
callback.run();
return null;
});
}
}
Loading
Loading