Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -131,13 +131,15 @@ public static IAuthorityFetcher getAuthorityFetcher() {
}

public static boolean invalidateCache(String username, String roleName) {
final boolean invalidated =
authorityFetcher.get().getAuthorCache().invalidateCache(username, roleName);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P1] Guard authority-cache refills with an invalidation generation

Reordering these invalidations only removes matcher entries produced by an in-flight match; it does not prevent the underlying authority cache from being refilled after its invalidation. A matcher miss holds the matcher read lock while checkCanSelectFromTable4Pipe() may issue a ConfigNode RPC. If a pre-revocation successful response returns after the author cache is cleared, ClusterAuthorityFetcher.checkPrivilegeFromConfigNode() can call putUserCache() with the old User while this thread is waiting for the matcher write lock. The following matcher invalidation then clears only the matcher entry, leaving the stale authority entry behind; the next event repopulates the matcher and a busy pipe can continue passing a revoked user.

Please add a generation/epoch to authority-cache loads (capture it before the RPC and only install the response if unchanged), or otherwise coordinate refills atomically with both invalidations. A latch-based test for this exact interleaving would prevent regression.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks for the detailed analysis. We understand the concern about authority-cache refill after invalidation.

This refill race is pre-existing and orthogonal to this PR: the unconditional putUserCache() path in ClusterAuthorityFetcher / BasicAuthorityCache is unchanged, and this PR is scoped to table-level matcher caching and its invalidation behavior. We do not plan to expand this PR into an authority-cache generation/epoch redesign.

If the maintainers consider it necessary, a separate PR can be opened later to handle authority-cache refill coordination and add the latch-based interleaving test there. For this PR, we would prefer to keep the matcher-related invalidation ordering fix and focused matcher tests.

PipeInsertionDataNodeListener.getInstance().invalidateAllCache();
return authorityFetcher.get().getAuthorCache().invalidateCache(username, roleName);
return invalidated;
}

public static void invalidateAllCache() {
PipeInsertionDataNodeListener.getInstance().invalidateAllCache();
authorityFetcher.get().getAuthorCache().invalidAllCache();
PipeInsertionDataNodeListener.getInstance().invalidateAllCache();
}

public static User getUser(String username) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public class CachedSchemaPatternMatcher implements PipeDataRegionMatcher {

// Use full cache to avoid queue stuck and block insertion
protected final Map<IDeviceID, Set<PipeRealtimeDataRegionSource>> deviceToSourcesCache;
protected final Map<Pair<String, IDeviceID>, Set<PipeRealtimeDataRegionSource>>
protected final Map<Pair<String, String>, Set<PipeRealtimeDataRegionSource>>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Prevent stale authorization results from refilling this cache

AuthorityChecker.invalidateCache() currently clears this matcher before invalidating the authority cache. That leaves a stale-refill window: a concurrent event can miss this cache, read the old authorization result, and cache a table-wide denial here. After the grant invalidation completes, later events can keep hitting that stale entry, remain unmatched, and advance progress until another invalidation or source change.

Please invalidate the authority cache before this matcher (also in invalidateAllCache()), or coordinate the two caches with a generation/version so that an entry computed before invalidation cannot be installed afterward.

databaseAndTableToSourcesCache;

public CachedSchemaPatternMatcher() {
Expand Down Expand Up @@ -102,7 +102,7 @@ public void deregister(final PipeRealtimeDataRegionSource source) {
public void invalidateCache() {
lock.writeLock().lock();
try {
// Will invalidate device cache
// The table-model cache also depends on access control, so it must be invalidated separately.
databaseAndTableToSourcesCache.clear();
} finally {
lock.writeLock().unlock();
Expand Down Expand Up @@ -144,6 +144,11 @@ public Pair<Set<PipeRealtimeDataRegionSource>, Set<PipeRealtimeDataRegionSource>
return new Pair<>(matchedSources, findUnmatchedSources(matchedSources));
}

// tableNames is also used for privilege checks on table-model TsFile events, so it must be
// complete even after every source has already matched.
final boolean isTableModelTsFileEvent =
event.getEvent() instanceof PipeTsFileInsertionEvent
&& ((PipeTsFileInsertionEvent) event.getEvent()).isTableModelEvent();
final Set<String> tableNames = new HashSet<>();
for (final Map.Entry<IDeviceID, String[]> entry : event.getSchemaInfo().entrySet()) {
final IDeviceID deviceID = entry.getKey();
Expand All @@ -154,24 +159,25 @@ public Pair<Set<PipeRealtimeDataRegionSource>, Set<PipeRealtimeDataRegionSource>
|| deviceID.getTableName().equals(PATH_ROOT)) {
matchTreeModelEvent(deviceID, entry.getValue(), matchedSources);
} else {
tableNames.add(deviceID.getTableName());
matchTableModelEvent(
event.getEvent() instanceof PipeInsertionEvent
? ((PipeInsertionEvent) event.getEvent()).getTableModelDatabaseName()
: null,
deviceID,
matchedSources);
final String tableName = deviceID.getTableName();
if (tableNames.add(tableName) && matchedSources.size() < sources.size()) {
final String tableModelDatabaseName =
event.getEvent() instanceof PipeInsertionEvent
? ((PipeInsertionEvent) event.getEvent()).getTableModelDatabaseName()
: null;
matchTableModelEvent(tableModelDatabaseName, tableName, matchedSources);
}
}

if (matchedSources.size() == sources.size()) {
if (matchedSources.size() == sources.size() && !isTableModelTsFileEvent) {
break;
}
}

if (event.getEvent() instanceof PipeTsFileInsertionEvent) {
final PipeTsFileInsertionEvent tsFileInsertionEvent =
(PipeTsFileInsertionEvent) event.getEvent();
if (tsFileInsertionEvent.isTableModelEvent()) {
if (isTableModelTsFileEvent) {
tsFileInsertionEvent.setTableNames(tableNames);
} else {
tsFileInsertionEvent.setTreeSchemaMap(event.getSchemaInfo());
Expand Down Expand Up @@ -273,7 +279,7 @@ protected Set<PipeRealtimeDataRegionSource> filterSourcesByDevice(final IDeviceI

protected void matchTableModelEvent(
final String databaseName,
final IDeviceID tableName,
final String tableName,
final Set<PipeRealtimeDataRegionSource> matchedSources) {
// this would not happen
if (databaseName == null) {
Expand All @@ -294,7 +300,7 @@ protected void matchTableModelEvent(
}

protected Set<PipeRealtimeDataRegionSource> filterSourcesByDatabaseAndTable(
final Pair<String, IDeviceID> databaseNameAndTableName) {
final Pair<String, String> databaseNameAndTableName) {
final Set<PipeRealtimeDataRegionSource> filteredSources = new HashSet<>();

for (final PipeRealtimeDataRegionSource source : sources) {
Expand All @@ -317,21 +323,20 @@ protected Set<PipeRealtimeDataRegionSource> filterSourcesByDatabaseAndTable(
}

private boolean matchesTablePattern(
final TablePattern tablePattern, final Pair<String, IDeviceID> databaseNameAndTableName) {
final TablePattern tablePattern, final Pair<String, String> databaseNameAndTableName) {
return Objects.isNull(tablePattern)
|| (tablePattern.isTableModelDataAllowedToBeCaptured()
&& tablePattern.matchesDatabase(databaseNameAndTableName.getLeft())
&& tablePattern.matchesTable(databaseNameAndTableName.getRight().getTableName()));
&& tablePattern.matchesTable(databaseNameAndTableName.getRight()));
}

private boolean notFilteredByAccess(
final UserEntity userEntity, final Pair<String, IDeviceID> databaseNameAndTableName) {
final UserEntity userEntity, final Pair<String, String> databaseNameAndTableName) {
return AuthorityChecker.getAccessControl()
.checkCanSelectFromTable4Pipe(
userEntity.getUsername(),
new QualifiedObjectName(
databaseNameAndTableName.getLeft(),
databaseNameAndTableName.getRight().getTableName()),
databaseNameAndTableName.getLeft(), databaseNameAndTableName.getRight()),
userEntity);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import org.apache.iotdb.commons.pipe.datastructure.pattern.PrefixTreePattern;
import org.apache.iotdb.commons.pipe.event.EnrichedEvent;
import org.apache.iotdb.db.conf.IoTDBDescriptor;
import org.apache.iotdb.db.pipe.event.common.PipeInsertionEvent;
import org.apache.iotdb.db.pipe.event.common.tsfile.PipeTsFileInsertionEvent;
import org.apache.iotdb.db.pipe.event.realtime.PipeRealtimeEvent;
import org.apache.iotdb.db.pipe.source.dataregion.realtime.PipeRealtimeDataRegionSource;
import org.apache.iotdb.db.pipe.source.dataregion.realtime.epoch.TsFileEpoch;
Expand All @@ -39,12 +41,18 @@
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
Expand Down Expand Up @@ -73,6 +81,25 @@ public boolean shouldParsePattern() {
}
}

private static class CountingCachedSchemaPatternMatcher extends CachedSchemaPatternMatcher {

private int tableMatchCount;

@Override
protected void matchTableModelEvent(
final String databaseName,
final String tableName,
final Set<PipeRealtimeDataRegionSource> matchedSources) {
++tableMatchCount;
// Simulate a successful table-level match so this test focuses on match orchestration.
matchedSources.addAll(sources);
}

private int getTableMatchCount() {
return tableMatchCount;
}
}

private CachedSchemaPatternMatcher matcher;
private ExecutorService executorService;
private List<PipeRealtimeDataRegionSource> extractors;
Expand Down Expand Up @@ -178,6 +205,51 @@ public void testCachedMatcher() throws Exception {
future.get();
}

@Test
public void testTableModelMatchesEachTableOncePerEvent() throws Exception {
final CountingCachedSchemaPatternMatcher countingMatcher =
new CountingCachedSchemaPatternMatcher();
final PipeRealtimeDataRegionSource source = new PipeRealtimeDataRegionFakeSource();
countingMatcher.register(source);

final PipeInsertionEvent insertionEvent = Mockito.mock(PipeInsertionEvent.class);
Mockito.when(insertionEvent.getTableModelDatabaseName()).thenReturn("db");
final Map<IDeviceID, String[]> schemaInfo = new LinkedHashMap<>();
schemaInfo.put(new StringArrayDeviceID("table1", "tag1"), new String[0]);
schemaInfo.put(new StringArrayDeviceID("table1", "tag2"), new String[0]);

Assert.assertTrue(
countingMatcher
.match(new MockedPipeRealtimeEvent(insertionEvent, null, schemaInfo))
.getLeft()
.contains(source));
Assert.assertEquals(1, countingMatcher.getTableMatchCount());
}

@Test
public void testMultiTableTsFileCollectsAllTableNamesAfterAllSourcesMatched() throws Exception {
final CountingCachedSchemaPatternMatcher countingMatcher =
new CountingCachedSchemaPatternMatcher();
final PipeRealtimeDataRegionSource source = new PipeRealtimeDataRegionFakeSource();
countingMatcher.register(source);

final PipeTsFileInsertionEvent tsFileInsertionEvent =
Mockito.mock(PipeTsFileInsertionEvent.class);
Mockito.when(tsFileInsertionEvent.isTableModelEvent()).thenReturn(true);
Mockito.when(tsFileInsertionEvent.getTableModelDatabaseName()).thenReturn("db");
final Map<IDeviceID, String[]> schemaInfo = new LinkedHashMap<>();
schemaInfo.put(new StringArrayDeviceID("table1", "tag1"), new String[0]);
schemaInfo.put(new StringArrayDeviceID("table2", "tag2"), new String[0]);

countingMatcher.match(new MockedPipeRealtimeEvent(tsFileInsertionEvent, null, schemaInfo));

final ArgumentCaptor<Set<String>> tableNamesCaptor = ArgumentCaptor.forClass(Set.class);
Mockito.verify(tsFileInsertionEvent).setTableNames(tableNamesCaptor.capture());
Assert.assertEquals(
new HashSet<>(Arrays.asList("table1", "table2")), tableNamesCaptor.getValue());
Assert.assertEquals(1, countingMatcher.getTableMatchCount());
}

public static class PipeRealtimeDataRegionFakeSource extends PipeRealtimeDataRegionSource {

public PipeRealtimeDataRegionFakeSource() {
Expand Down
Loading