IdNameIndex.java
// 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.datasource.metacache;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
import java.util.concurrent.ConcurrentMap;
/**
* Runtime-only one-to-one index between metadata IDs and canonical local names.
*
* <p>Reads by ID are lock-free. All mutations are serialized so the two maps have a stable one-to-one relationship
* after each mutation completes. The reverse map is intentionally kept internal: production callers use it only
* indirectly for uniqueness validation and constant-time removal by name.
*/
public final class IdNameIndex {
private final String scope;
private final ConcurrentMap<Long, String> idToName = Maps.newConcurrentMap();
private final ConcurrentMap<String, Long> nameToId = Maps.newConcurrentMap();
public IdNameIndex(String scope) {
this.scope = Objects.requireNonNull(scope, "scope can not be null");
}
@Nullable
public String getName(long id) {
return idToName.get(id);
}
/**
* Validate one identity without publishing it.
*
* <p>Callers use this before mutating names/object caches so a pre-existing identity conflict fails without
* leaving those caches partially updated. The final {@link #put(long, String)} must still run inside the
* MetaCacheEntry publication action to keep the ID side effect ordered with object invalidation.
*/
public void checkCanPut(long id, String name) {
Objects.requireNonNull(name, "name can not be null");
Long existingId = nameToId.get(name);
String existingName = idToName.get(id);
// Keep the common exact-match and cold-index paths lock-free. A later put remains the authoritative
// validation inside the caller's MetaCacheEntry publication window.
if ((existingId == null && existingName == null)
|| (Long.valueOf(id).equals(existingId) && name.equals(existingName))) {
return;
}
// Re-read suspicious snapshots under the mutation lock. This both confirms real conflicts and avoids
// rejecting a reader that observed the two maps between the writes of one synchronized mutation.
synchronized (this) {
checkCanPutLocked(id, name);
}
}
private void checkCanPutLocked(long id, String name) {
Long existingId = nameToId.get(name);
String existingName = idToName.get(id);
Preconditions.checkState(existingId == null || existingId == id,
"%s name '%s' is already mapped to id %s, can not map to id %s",
scope, name, existingId, id);
Preconditions.checkState(existingName == null || existingName.equals(name),
"%s id %s is already mapped to name '%s', can not map to name '%s'",
scope, id, existingName, name);
Preconditions.checkState((existingId == null) == (existingName == null),
"%s index is inconsistent for id %s and name '%s': existing id %s, existing name '%s'",
scope, id, name, existingId, existingName);
}
/**
* Publish one identity. Re-publishing the same pair is idempotent; every other conflict is a correctness error.
*/
public synchronized void put(long id, String name) {
Objects.requireNonNull(name, "name can not be null");
checkCanPutLocked(id, name);
idToName.put(id, name);
nameToId.put(name, id);
}
/**
* Remove the identity associated with a canonical local name in constant time.
*/
public synchronized void removeName(String name) {
Objects.requireNonNull(name, "name can not be null");
Long id = nameToId.get(name);
if (id == null) {
return;
}
String indexedName = idToName.get(id);
Preconditions.checkState(name.equals(indexedName),
"%s index is inconsistent for name '%s' and id %s: indexed name '%s'",
scope, name, id, indexedName);
nameToId.remove(name);
boolean removed = idToName.remove(id, name);
Preconditions.checkState(removed,
"%s index failed to remove id %s and name '%s'", scope, id, name);
}
/**
* Find a retained canonical name for the mode-2 cache-only invalidation fallback.
*
* <p>This deliberately preserves the existing O(N) fallback behavior. Normal lookup and exact-name cleanup do
* not use this method.
*/
@Nullable
public String findNameIgnoreCase(String requestedName) {
Objects.requireNonNull(requestedName, "requestedName can not be null");
return idToName.values().stream()
.filter(name -> name.equalsIgnoreCase(requestedName))
.findFirst()
.orElse(null);
}
public synchronized void clear() {
idToName.clear();
nameToId.clear();
}
@VisibleForTesting
public synchronized boolean containsMappingForTest(long id, String name) {
return name.equals(idToName.get(id)) && Long.valueOf(id).equals(nameToId.get(name));
}
@VisibleForTesting
public synchronized int sizeForTest() {
Preconditions.checkState(idToName.size() == nameToId.size(),
"%s index size mismatch: idToName=%s, nameToId=%s",
scope, idToName.size(), nameToId.size());
return idToName.size();
}
}