Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import org.apache.accumulo.core.client.AccumuloException;
import org.apache.accumulo.core.client.AccumuloSecurityException;
import org.apache.accumulo.core.client.TableNotFoundException;
import org.apache.accumulo.core.conf.ClientProperty;
import org.apache.accumulo.core.data.InstanceId;
import org.apache.accumulo.core.data.Mutation;
import org.apache.accumulo.core.data.Range;
Expand Down Expand Up @@ -88,15 +90,17 @@ public abstract List<Range> binRanges(ClientContext context, List<Range> ranges,
private static class LocatorKey {
InstanceId instanceId;
TableId tableId;
Duration cacheExpiration;

LocatorKey(InstanceId instanceId, TableId table) {
LocatorKey(InstanceId instanceId, TableId table, Duration cacheExpiration) {
this.instanceId = instanceId;
this.tableId = table;
this.cacheExpiration = cacheExpiration;
}

@Override
public int hashCode() {
return instanceId.hashCode() + tableId.hashCode();
return Objects.hash(instanceId, tableId, cacheExpiration);
}

@Override
Expand All @@ -108,7 +112,8 @@ public boolean equals(Object o) {
}

public boolean equals(LocatorKey lk) {
return instanceId.equals(lk.instanceId) && tableId.equals(lk.tableId);
return instanceId.equals(lk.instanceId) && tableId.equals(lk.tableId)
&& cacheExpiration.equals(lk.cacheExpiration);
}

}
Expand Down Expand Up @@ -146,7 +151,9 @@ public static synchronized TabletLocator getLocator(ClientContext context, Table
clearUnusedTables(context);

TableState state = context.getTableState(tableId);
LocatorKey key = new LocatorKey(context.getInstanceID(), tableId);
Duration cacheExpiration = Duration.ofMillis(Objects.requireNonNull(
ClientProperty.CLIENT_EXTENT_CACHE_EXPIRATION.getTimeInMillis(context.getProperties())));
LocatorKey key = new LocatorKey(context.getInstanceID(), tableId, cacheExpiration);
if (state == TableState.OFFLINE) {
locators.remove(key);
return offlineLocators.computeIfAbsent(key,
Expand All @@ -161,10 +168,10 @@ public static synchronized TabletLocator getLocator(ClientContext context, Table
tl = new RootTabletLocator(context.getTServerLockChecker());
} else if (MetadataTable.ID.equals(tableId)) {
tl = new TabletLocatorImpl(MetadataTable.ID, getLocator(context, RootTable.ID), mlo,
context.getTServerLockChecker());
context.getTServerLockChecker(), cacheExpiration);
} else {
tl = new TabletLocatorImpl(tableId, getLocator(context, MetadataTable.ID), mlo,
context.getTServerLockChecker());
context.getTServerLockChecker(), cacheExpiration);
}
locators.put(key, tl);
}
Expand All @@ -178,7 +185,9 @@ public static synchronized TabletLocator getLocator(ClientContext context, Table
*/
@VisibleForTesting
public static synchronized boolean isPresent(ClientContext context, TableId tableId) {
LocatorKey key = new LocatorKey(context.getInstanceID(), tableId);
Duration cacheExpiration = Duration.ofMillis(Objects.requireNonNull(
ClientProperty.CLIENT_EXTENT_CACHE_EXPIRATION.getTimeInMillis(context.getProperties())));
LocatorKey key = new LocatorKey(context.getInstanceID(), tableId, cacheExpiration);
return locators.containsKey(key) || offlineLocators.containsKey(key);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.apache.accumulo.core.util.UtilWaitThread.sleepUninterruptibly;

import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
Expand All @@ -34,6 +35,7 @@
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

Expand All @@ -54,12 +56,16 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalCause;
import com.github.benmanes.caffeine.cache.Scheduler;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

public class TabletLocatorImpl extends TabletLocator {

private static final Logger log = LoggerFactory.getLogger(TabletLocatorImpl.class);

// MAX_TEXT represents a TEXT object that is greater than all others. Attempted to use null for
// this purpose, but there seems to be a bug in TreeMap.tailMap with null. Therefore instead of
// using null, created MAX_TEXT.
Expand All @@ -80,7 +86,12 @@ public class TabletLocatorImpl extends TabletLocator {

protected TableId tableId;
protected TabletLocator parent;
// The TreeMap supports range lookups; Caffeine tracks access and expires entries from it.
protected TreeMap<Text,TabletLocation> metaCache = new TreeMap<>(END_ROW_COMPARATOR);
// Null when extent expiration is disabled.
private final Cache<KeyExtent,TabletLocation> extentCache;
private final ConcurrentLinkedQueue<TabletLocation> evictedExtents =
new ConcurrentLinkedQueue<>();
protected TabletLocationObtainer locationObtainer;
private final TabletServerLockChecker lockChecker;
protected Text lastTabletRow;
Expand Down Expand Up @@ -153,15 +164,53 @@ private TabletLocation checkLock(TabletLocation tl) {

public TabletLocatorImpl(TableId tableId, TabletLocator parent, TabletLocationObtainer tlo,
TabletServerLockChecker tslc) {
this(tableId, parent, tlo, tslc, Duration.ZERO);
}

public TabletLocatorImpl(TableId tableId, TabletLocator parent, TabletLocationObtainer tlo,
TabletServerLockChecker tslc, Duration cacheExpiration) {
this.tableId = tableId;
this.parent = parent;
this.locationObtainer = tlo;
this.lockChecker = tslc;

extentCache = cacheExpiration.isZero() ? null
: Caffeine.newBuilder().expireAfterAccess(cacheExpiration)
.scheduler(Scheduler.systemScheduler()).evictionListener(this::onExtentEviction)
.build();

this.lastTabletRow = new Text(tableId.canonical());
lastTabletRow.append(new byte[] {'<'}, 0, 1);
}

private void onExtentEviction(KeyExtent extent, TabletLocation location, RemovalCause cause) {
// The listener may run while this thread holds rLock, so queue the eviction before attempting
// the write lock processInvalidated() drains the queue later if the lock is unavailable here
evictedExtents.add(location);
try {
if (wLock.tryLock(1, MILLISECONDS)) {
try {
processEvictedExtents();
} finally {
wLock.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}

private void processEvictedExtents() {
TabletLocation evicted;
while ((evicted = evictedExtents.poll()) != null) {
Text endRow =
evicted.tablet_extent.endRow() == null ? MAX_TEXT : evicted.tablet_extent.endRow();
if (metaCache.get(endRow) == evicted) {
metaCache.remove(endRow);
}
}
}

@Override
public <T extends Mutation> void binMutations(ClientContext context, List<T> mutations,
Map<String,TabletServerMutations<T>> binnedMutations, List<T> failures)
Expand Down Expand Up @@ -465,6 +514,9 @@ public void invalidateCache() {
try {
invalidatedCount = metaCache.size();
metaCache.clear();
if (extentCache != null) {
extentCache.invalidateAll();
}
} finally {
wLock.unlock();
}
Expand Down Expand Up @@ -596,6 +648,9 @@ private void updateCache(TabletLocation tabletLocation, LockCheckerSession lcSes
er = MAX_TEXT;
}
metaCache.put(er, tabletLocation);
if (extentCache != null) {
extentCache.put(tabletLocation.tablet_extent, tabletLocation);
}

if (!badExtents.isEmpty()) {
removeOverlapping(badExtents, tabletLocation.tablet_extent);
Expand Down Expand Up @@ -648,9 +703,14 @@ private TabletLocation locateTabletInCache(Text row) {
Entry<Text,TabletLocation> entry = metaCache.ceilingEntry(row);

if (entry != null) {
KeyExtent ke = entry.getValue().tablet_extent;
TabletLocation location = extentCache == null ? entry.getValue()
: extentCache.getIfPresent(entry.getValue().tablet_extent);
if (location == null) {
return null;
}
KeyExtent ke = location.tablet_extent;
if (ke.prevEndRow() == null || ke.prevEndRow().compareTo(row) < 0) {
return entry.getValue();
return location;
}
}
return null;
Expand Down Expand Up @@ -714,7 +774,7 @@ private TabletLocation processInvalidatedAndCheckLock(ClientContext context,
private void processInvalidated(ClientContext context, LockCheckerSession lcSession)
throws AccumuloSecurityException, AccumuloException, TableNotFoundException {

if (badExtents.isEmpty() && badServers.isEmpty()) {
if (badExtents.isEmpty() && badServers.isEmpty() && evictedExtents.isEmpty()) {
return;
}

Expand All @@ -723,11 +783,16 @@ private void processInvalidated(ClientContext context, LockCheckerSession lcSess
if (!writeLockHeld) {
rLock.unlock();
wLock.lock();
if (badExtents.isEmpty() && badServers.isEmpty()) {
if (badExtents.isEmpty() && badServers.isEmpty() && evictedExtents.isEmpty()) {
return;
}
}

processEvictedExtents();
if (badExtents.isEmpty() && badServers.isEmpty()) {
return;
}

List<Range> lookups = new ArrayList<>(badExtents.size());

for (KeyExtent be : badExtents) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ public enum ClientProperty {
"The maximum duration to leave idle transports open in the client's transport pool", "2.1.0",
false),

CLIENT_EXTENT_CACHE_EXPIRATION("client.extent.cache.expiration", "0", PropertyType.TIMEDURATION,
"The expiration time for cached tablet extents. A value of 0 disables expiration.", "2.1.7",
false),

// Trace
@Deprecated(since = "2.1.0", forRemoval = true)
TRACE_SPAN_RECEIVERS("trace.span.receivers", "org.apache.accumulo.tracer.ZooTraceClient",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ public enum PropertyType {
TIMEDURATION("duration", boundedUnits(0, Long.MAX_VALUE, true, "", "ms", "s", "m", "h", "d"),
"A non-negative integer optionally followed by a unit of time (whitespace"
+ " disallowed), as in 30s.\n"
+ "If no unit of time is specified, seconds are assumed. Valid units"
+ " are 'ms', 's', 'm', 'h' for milliseconds, seconds, minutes, and hours.\n"
+ "If no unit of time is specified, seconds are assumed. Valid suffixes (units)"
+ " are 'ms', 's', 'm', 'h', 'd' for milliseconds, seconds, minutes, hours, and days.\n"
+ "Examples of valid durations are '600', '30s', '45m', '30000ms', '3d', and '1h'.\n"
+ "Examples of invalid durations are '1w', '1h30m', '1s 200ms', 'ms', '',"
+ " and 'a'.\nUnless otherwise stated, the max value for the duration"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* 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
*
* https://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.accumulo.core.clientImpl;

import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.replay;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertSame;

import java.util.Properties;

import org.apache.accumulo.core.conf.ClientProperty;
import org.apache.accumulo.core.data.InstanceId;
import org.apache.accumulo.core.manager.state.tables.TableState;
import org.apache.accumulo.core.metadata.RootTable;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

public class TabletLocatorTest {

private static final InstanceId INSTANCE_ID = InstanceId.of("instance");

@AfterEach
public void clearLocators() {
TabletLocator.clearLocators();
}

@Test
public void testExpirationIsPartOfLocatorIdentity() {
TabletLocator noExpiration = TabletLocator.getLocator(createMockedContext("0"), RootTable.ID);
TabletLocator tenMinutes = TabletLocator.getLocator(createMockedContext("10m"), RootTable.ID);

assertSame(noExpiration, TabletLocator.getLocator(createMockedContext("0"), RootTable.ID));
assertNotSame(noExpiration, tenMinutes);
assertSame(tenMinutes, TabletLocator.getLocator(createMockedContext("600s"), RootTable.ID));
}

private ClientContext createMockedContext(String expiration) {
Properties properties = new Properties();
properties.setProperty(ClientProperty.CLIENT_EXTENT_CACHE_EXPIRATION.getKey(), expiration);

ClientContext context = createMock(ClientContext.class);
expect(context.getTableState(RootTable.ID)).andReturn(TableState.ONLINE).anyTimes();
expect(context.getProperties()).andReturn(properties).anyTimes();
expect(context.getInstanceID()).andReturn(INSTANCE_ID).anyTimes();
expect(context.getTServerLockChecker()).andReturn(createMock(ZookeeperLockChecker.class))
.anyTimes();
replay(context);
return context;
}
}