3
3
*
4
4
* This source file is part of the FoundationDB open source project
5
5
*
6
- * Copyright 2015-2024 Apple Inc. and the FoundationDB project authors
6
+ * Copyright 2015-2025 Apple Inc. and the FoundationDB project authors
7
7
*
8
8
* Licensed under the Apache License, Version 2.0 (the "License");
9
9
* you may not use this file except in compliance with the License.
21
21
package com .apple .foundationdb .record .lucene ;
22
22
23
23
import com .apple .foundationdb .async .AsyncUtil ;
24
- import com .apple .foundationdb .record .ExecuteProperties ;
25
- import com .apple .foundationdb .record .IsolationLevel ;
26
24
import com .apple .foundationdb .record .RecordCursor ;
27
25
import com .apple .foundationdb .record .RecordCursorResult ;
28
- import com .apple .foundationdb .record .ScanProperties ;
29
- import com .apple .foundationdb .record .TupleRange ;
30
26
import com .apple .foundationdb .record .logging .KeyValueLogMessage ;
31
27
import com .apple .foundationdb .record .logging .LogMessageKeys ;
32
28
import com .apple .foundationdb .record .lucene .directory .FDBDirectoryManager ;
33
29
import com .apple .foundationdb .record .metadata .Index ;
34
30
import com .apple .foundationdb .record .metadata .RecordType ;
35
31
import com .apple .foundationdb .record .metadata .expressions .KeyExpression ;
32
+ import com .apple .foundationdb .record .provider .foundationdb .FDBIndexableRecord ;
36
33
import com .apple .foundationdb .record .provider .foundationdb .FDBRecordStore ;
37
34
import com .apple .foundationdb .record .provider .foundationdb .FDBStoreTimer ;
38
35
import com .apple .foundationdb .record .provider .foundationdb .FDBStoredRecord ;
39
- import com .apple .foundationdb .record .provider .foundationdb .IndexScrubbingTools ;
36
+ import com .apple .foundationdb .record .provider .foundationdb .FDBSyntheticRecord ;
37
+ import com .apple .foundationdb .record .provider .foundationdb .indexes .ValueIndexScrubbingToolsMissing ;
38
+ import com .apple .foundationdb .record .query .plan .RecordQueryPlanner ;
39
+ import com .apple .foundationdb .record .query .plan .synthetic .SyntheticRecordFromStoredRecordPlan ;
40
+ import com .apple .foundationdb .record .query .plan .synthetic .SyntheticRecordPlanner ;
40
41
import com .apple .foundationdb .record .util .pair .Pair ;
41
42
import com .apple .foundationdb .tuple .Tuple ;
42
43
import com .google .protobuf .Message ;
47
48
import java .io .IOException ;
48
49
import java .util .Collection ;
49
50
import java .util .Collections ;
50
- import java .util .HashMap ;
51
51
import java .util .List ;
52
52
import java .util .Map ;
53
- import java .util .Optional ;
54
53
import java .util .concurrent .CompletableFuture ;
54
+ import java .util .concurrent .atomic .AtomicReference ;
55
55
import java .util .stream .Collectors ;
56
56
57
57
/**
58
58
* Index Scrubbing Toolbox for a Lucene index maintainer. Scrub missing value index entries - i.e. detect record(s) that should
59
- * cannot be found in the segment index.
59
+ * have been indexed, but cannot be found in the segment index.
60
60
*/
61
- public class LuceneIndexScrubbingToolsMissing implements IndexScrubbingTools < FDBStoredRecord < Message >> {
61
+ public class LuceneIndexScrubbingToolsMissing extends ValueIndexScrubbingToolsMissing {
62
62
private Collection <RecordType > recordTypes = null ;
63
63
private Index index ;
64
+ private boolean isSynthetic ;
64
65
65
66
@ Nonnull
66
67
private final LucenePartitioner partitioner ;
@@ -81,23 +82,9 @@ public LuceneIndexScrubbingToolsMissing(@Nonnull LucenePartitioner partitioner,
81
82
public void presetCommonParams (Index index , boolean allowRepair , boolean isSynthetic , Collection <RecordType > types ) {
82
83
this .recordTypes = types ;
83
84
this .index = index ;
84
- }
85
-
86
- @ Override
87
- public RecordCursor <FDBStoredRecord <Message >> getCursor (final TupleRange range , final FDBRecordStore store , final int limit ) {
88
- final IsolationLevel isolationLevel = IsolationLevel .SNAPSHOT ;
89
- final ExecuteProperties .Builder executeProperties = ExecuteProperties .newBuilder ()
90
- .setIsolationLevel (isolationLevel )
91
- .setReturnedRowLimit (limit );
92
-
93
- final ScanProperties scanProperties = new ScanProperties (executeProperties .build (), false );
94
- return store .scanRecords (range , null , scanProperties );
95
- }
96
-
97
- @ Override
98
- public Tuple getKeyFromCursorResult (final RecordCursorResult <FDBStoredRecord <Message >> result ) {
99
- final FDBStoredRecord <Message > storedRecord = result .get ();
100
- return storedRecord == null ? null : storedRecord .getPrimaryKey ();
85
+ this .isSynthetic = isSynthetic ;
86
+ // call super, but force allowRepair as false
87
+ super .presetCommonParams (index , false , isSynthetic , types );
101
88
}
102
89
103
90
/**
@@ -110,6 +97,7 @@ public enum MissingIndexReason {
110
97
}
111
98
112
99
@ Override
100
+ @ Nullable
113
101
public CompletableFuture <Issue > handleOneItem (final FDBRecordStore store , final RecordCursorResult <FDBStoredRecord <Message >> result ) {
114
102
if (recordTypes == null || index == null ) {
115
103
throw new IllegalStateException ("presetParams was not called appropriately for this scrubbing tool" );
@@ -120,12 +108,12 @@ public CompletableFuture<Issue> handleOneItem(final FDBRecordStore store, final
120
108
return CompletableFuture .completedFuture (null );
121
109
}
122
110
123
- return detectMissingIndexKeys (rec )
111
+ return detectMissingIndexKeys (store , rec )
124
112
.thenApply (missingIndexesKeys -> {
125
113
if (missingIndexesKeys == null ) {
126
114
return null ;
127
115
}
128
- // Here: Oh, No! the index is missing!!
116
+ // Here: Oh, No! an index entry is missing!!
129
117
// (Maybe) report an error
130
118
return new Issue (
131
119
KeyValueLogMessage .build ("Scrubber: missing index entry" ,
@@ -137,59 +125,78 @@ public CompletableFuture<Issue> handleOneItem(final FDBRecordStore store, final
137
125
});
138
126
}
139
127
140
- public CompletableFuture <Pair <MissingIndexReason , Tuple >> detectMissingIndexKeys (FDBStoredRecord <Message > rec ) {
141
- // return the first missing (if any).
128
+ @ SuppressWarnings ("PMD.CloseResource" )
129
+ private CompletableFuture <Pair <MissingIndexReason , Tuple >> detectMissingIndexKeys (final FDBRecordStore store , FDBStoredRecord <Message > rec ) {
130
+ // Generate synthetic record (if applicable) and return the first detected missing (if any).
131
+ final AtomicReference <Pair <MissingIndexReason , Tuple >> issue = new AtomicReference <>();
132
+
133
+ if (!isSynthetic ) {
134
+ return checkMissingIndexKey (rec , issue ).thenApply (ignore -> issue .get ());
135
+ }
136
+ final RecordQueryPlanner queryPlanner =
137
+ new RecordQueryPlanner (store .getRecordMetaData (), store .getRecordStoreState ().withWriteOnlyIndexes (Collections .singletonList (index .getName ())));
138
+ final SyntheticRecordPlanner syntheticPlanner = new SyntheticRecordPlanner (store , queryPlanner );
139
+ SyntheticRecordFromStoredRecordPlan syntheticPlan = syntheticPlanner .forIndex (index );
140
+ final RecordCursor <FDBSyntheticRecord > recordCursor = syntheticPlan .execute (store , rec );
141
+
142
+ return AsyncUtil .whenAll (
143
+ recordCursor .asStream ().map (syntheticRecord -> checkMissingIndexKey (syntheticRecord , issue ))
144
+ .collect (Collectors .toList ()))
145
+ .whenComplete ((ret , e ) -> recordCursor .close ())
146
+ .thenApply (ignore -> issue .get ());
147
+
148
+ }
149
+
150
+ private CompletableFuture <Void > checkMissingIndexKey (FDBIndexableRecord <Message > rec ,
151
+ AtomicReference <Pair <MissingIndexReason , Tuple >> issue ) {
152
+ // Iterate grouping keys (if any) and detect missing index entry (if any)
142
153
final KeyExpression root = index .getRootExpression ();
143
154
final Map <Tuple , List <LuceneDocumentFromRecord .DocumentField >> recordFields = LuceneDocumentFromRecord .getRecordFields (root , rec );
144
155
if (recordFields .isEmpty ()) {
145
- // Could recordFields be an empty map?
146
- return CompletableFuture .completedFuture (Pair .of (MissingIndexReason .EMPTY_RECORDS_FIELDS , null ));
156
+ // recordFields should not be an empty map
157
+ issue .compareAndSet (null , Pair .of (MissingIndexReason .EMPTY_RECORDS_FIELDS , null ));
158
+ return AsyncUtil .DONE ;
147
159
}
148
160
if (recordFields .size () == 1 ) {
149
- // A single grouping key
150
- return checkMissingIndexKey (rec , recordFields .keySet ().stream ().findFirst ().get () );
161
+ // A single grouping key, simple check.
162
+ return checkMissingIndexKey (rec , recordFields .keySet ().stream ().iterator ().next (), issue );
151
163
}
152
164
153
- // Here: more than one grouping key
154
- final Map <Tuple , MissingIndexReason > keys = Collections .synchronizedMap (new HashMap <>());
165
+ // Here: more than one grouping key, declare an issue if at least one of them is missing
155
166
return AsyncUtil .whenAll ( recordFields .keySet ().stream ().map (groupingKey ->
156
- checkMissingIndexKey (rec , groupingKey )
157
- .thenApply (missing -> keys .put (missing .getValue (), missing .getKey ()))
167
+ checkMissingIndexKey (rec , groupingKey , issue )
158
168
).collect (Collectors .toList ()))
159
- .thenApply (ignore -> {
160
- final Optional <Map .Entry <Tuple , MissingIndexReason >> first = keys .entrySet ().stream ().findFirst ();
161
- return first .map (tupleStringEntry -> Pair .of (tupleStringEntry .getValue (), tupleStringEntry .getKey ())).orElse (null );
162
- });
169
+ .thenApply (ignore -> null );
163
170
}
164
171
165
- private CompletableFuture <Pair <MissingIndexReason , Tuple >> checkMissingIndexKey (FDBStoredRecord <Message > rec , Tuple groupingKey ) {
172
+ private CompletableFuture <Void > checkMissingIndexKey (FDBIndexableRecord <Message > rec , Tuple groupingKey , AtomicReference <Pair <MissingIndexReason , Tuple >> issue ) {
173
+ // Get partition (if applicable) and detect missing index entry (if any)
166
174
if (!partitioner .isPartitioningEnabled ()) {
167
- return CompletableFuture . completedFuture (
168
- isMissingIndexKey ( rec , null , groupingKey ) ?
169
- Pair . of ( MissingIndexReason . NOT_IN_PK_SEGMENT_INDEX , null ) :
170
- null ) ;
175
+ if ( isMissingIndexKey ( rec , null , groupingKey )) {
176
+ issue . compareAndSet ( null , Pair . of ( MissingIndexReason . NOT_IN_PK_SEGMENT_INDEX , null ));
177
+ }
178
+ return AsyncUtil . DONE ;
171
179
}
172
180
return partitioner .tryGetPartitionInfo (rec , groupingKey ).thenApply (partitionInfo -> {
173
181
if (partitionInfo == null ) {
174
- return Pair .of (MissingIndexReason .NOT_IN_PARTITION , groupingKey );
175
- }
176
- if (isMissingIndexKey (rec , partitionInfo .getId (), groupingKey )) {
177
- return Pair .of (MissingIndexReason .NOT_IN_PK_SEGMENT_INDEX , groupingKey );
182
+ issue .compareAndSet (null , Pair .of (MissingIndexReason .NOT_IN_PARTITION , groupingKey ));
183
+ } else if (isMissingIndexKey (rec , partitionInfo .getId (), groupingKey )) {
184
+ issue .compareAndSet (null , Pair .of (MissingIndexReason .NOT_IN_PK_SEGMENT_INDEX , groupingKey ));
178
185
}
179
186
return null ;
180
187
});
181
188
}
182
189
183
190
@ SuppressWarnings ("PMD.CloseResource" )
184
- private boolean isMissingIndexKey (FDBStoredRecord <Message > rec , Integer partitionId , Tuple groupingKey ) {
191
+ private boolean isMissingIndexKey (FDBIndexableRecord <Message > rec , Integer partitionId , Tuple groupingKey ) {
185
192
@ Nullable final LucenePrimaryKeySegmentIndex segmentIndex = directoryManager .getDirectory (groupingKey , partitionId ).getPrimaryKeySegmentIndex ();
186
193
if (segmentIndex == null ) {
187
- // Here: iternal error, getIndexScrubbingTools should have indicated that scrub missing is not supported.
188
- throw new IllegalStateException ("This scrubber should not have been used " );
194
+ // Here: internal error, getIndexScrubbingTools should have indicated that scrub missing is not supported.
195
+ throw new IllegalStateException ("LucneIndexScrubbingToolsMissing without a LucenePrimaryKeySegmentIndex " );
189
196
}
190
197
191
198
try {
192
- // TODO: this is called to initilize the writer, else we get an exception at getDirectoryReader. Should it really be done for a RO operation?
199
+ // TODO: this is called to initialize the writer, else we get an exception at getDirectoryReader. Should it really be done for a RO operation?
193
200
directoryManager .getIndexWriter (groupingKey , partitionId , indexAnalyzerSelector .provideIndexAnalyzer ("" ));
194
201
} catch (IOException e ) {
195
202
throw LuceneExceptions .toRecordCoreException ("failed getIndexWriter" , e );
@@ -202,7 +209,7 @@ private boolean isMissingIndexKey(FDBStoredRecord<Message> rec, Integer partitio
202
209
return true ;
203
210
}
204
211
} catch (IOException ex ) {
205
- // Here: probably an fdb exception. Unwrap and rethrow.
212
+ // Here: an unexpected exception. Unwrap and rethrow.
206
213
throw LuceneExceptions .toRecordCoreException ("Error while finding document" , ex );
207
214
}
208
215
return false ;
0 commit comments