|
20 | 20 | import org.apache.fluss.client.Connection; |
21 | 21 | import org.apache.fluss.client.ConnectionFactory; |
22 | 22 | import org.apache.fluss.client.admin.Admin; |
| 23 | +import org.apache.fluss.client.table.Table; |
| 24 | +import org.apache.fluss.client.table.scanner.log.LogScanner; |
23 | 25 | import org.apache.fluss.config.ConfigOptions; |
24 | 26 | import org.apache.fluss.config.Configuration; |
25 | 27 | import org.apache.fluss.exception.NetworkException; |
26 | 28 | import org.apache.fluss.flink.sink.serializer.RowDataSerializationSchema; |
27 | 29 | import org.apache.fluss.flink.utils.FlinkTestBase; |
28 | 30 | import org.apache.fluss.metadata.DatabaseDescriptor; |
29 | 31 | import org.apache.fluss.metadata.Schema; |
| 32 | +import org.apache.fluss.metadata.TableBucket; |
30 | 33 | import org.apache.fluss.metadata.TableChange; |
31 | 34 | import org.apache.fluss.metadata.TableDescriptor; |
32 | 35 | import org.apache.fluss.metadata.TablePath; |
|
51 | 54 | import org.junit.jupiter.params.ParameterizedTest; |
52 | 55 | import org.junit.jupiter.params.provider.ValueSource; |
53 | 56 |
|
| 57 | +import java.lang.reflect.Field; |
| 58 | +import java.time.Duration; |
54 | 59 | import java.util.Collections; |
| 60 | +import java.util.HashMap; |
| 61 | +import java.util.Map; |
| 62 | +import java.util.Set; |
55 | 63 | import java.util.function.BiConsumer; |
56 | 64 |
|
57 | 65 | import static org.assertj.core.api.Assertions.assertThat; |
@@ -265,6 +273,137 @@ private FlinkSinkWriter<RowData> createSinkWriter( |
265 | 273 | serializationSchema); |
266 | 274 | } |
267 | 275 |
|
| 276 | + @Test |
| 277 | + void testTableInfoAutoUpdate() throws Exception { |
| 278 | + String testDb = "test-auto-update-db"; |
| 279 | + TablePath testTablePath = TablePath.of(testDb, "test-auto-update-table"); |
| 280 | + |
| 281 | + // Create database |
| 282 | + admin.createDatabase(testDb, DatabaseDescriptor.EMPTY, true).get(); |
| 283 | + |
| 284 | + // Create log table with 3 buckets (no primary key) |
| 285 | + TableDescriptor tableDescriptor = |
| 286 | + TableDescriptor.builder() |
| 287 | + .schema( |
| 288 | + Schema.newBuilder() |
| 289 | + .column("id", DataTypes.INT()) |
| 290 | + .column("name", DataTypes.STRING()) |
| 291 | + .build()) |
| 292 | + .distributedBy(3) |
| 293 | + .build(); |
| 294 | + createTable(testTablePath, tableDescriptor); |
| 295 | + |
| 296 | + Configuration clientConfig = FLUSS_CLUSTER_EXTENSION.getClientConfig(); |
| 297 | + MockWriterInitContext mockWriterInitContext = |
| 298 | + new MockWriterInitContext(new InterceptingOperatorMetricGroup()); |
| 299 | + |
| 300 | + // Create AppendSinkWriter |
| 301 | + RowType tableRowType = |
| 302 | + RowType.of( |
| 303 | + new LogicalType[] {new IntType(), new CharType(10)}, |
| 304 | + new String[] {"id", "name"}); |
| 305 | + RowDataSerializationSchema serializationSchema = |
| 306 | + new RowDataSerializationSchema(true, false); |
| 307 | + AppendSinkWriter<RowData> writer = |
| 308 | + new AppendSinkWriter<>( |
| 309 | + testTablePath, |
| 310 | + clientConfig, |
| 311 | + tableRowType, |
| 312 | + mockWriterInitContext.getMailboxExecutor(), |
| 313 | + serializationSchema); |
| 314 | + |
| 315 | + try { |
| 316 | + writer.initialize(mockWriterInitContext.metricGroup()); |
| 317 | + |
| 318 | + // Step 1: Write data with 3 buckets, verify success |
| 319 | + for (int i = 0; i < 10; i++) { |
| 320 | + writer.write( |
| 321 | + GenericRowData.of(i, StringData.fromString("name" + i)), |
| 322 | + new MockSinkWriterContext()); |
| 323 | + } |
| 324 | + writer.flush(false); |
| 325 | + |
| 326 | + // Verify data is written to 3 buckets |
| 327 | + Map<Integer, Integer> bucketCounts = countRecordsPerBucket(testTablePath, 3); |
| 328 | + assertThat(bucketCounts.size()).isEqualTo(3); |
| 329 | + int totalRecords = bucketCounts.values().stream().mapToInt(Integer::intValue).sum(); |
| 330 | + assertThat(totalRecords).isEqualTo(10); |
| 331 | + |
| 332 | + // Step 2: Alter table bucket number to 4 |
| 333 | + admin.alterTable( |
| 334 | + testTablePath, |
| 335 | + Collections.singletonList(TableChange.set("bucket.num", "4")), |
| 336 | + false) |
| 337 | + .get(); |
| 338 | + |
| 339 | + // Wait for schema sync |
| 340 | + FLUSS_CLUSTER_EXTENSION.waitAllSchemaSync(testTablePath, 2); |
| 341 | + |
| 342 | + // Step 3: Force update table by setting lastRefreshTime to trigger refresh |
| 343 | + Field lastRefreshTimeField = FlinkSinkWriter.class.getDeclaredField("lastRefreshTime"); |
| 344 | + lastRefreshTimeField.setAccessible(true); |
| 345 | + lastRefreshTimeField.set( |
| 346 | + writer, System.currentTimeMillis() - 61000); // Set to 61 seconds ago |
| 347 | + |
| 348 | + // Step 4: Write more data, should use 4 buckets now |
| 349 | + for (int i = 10; i < 20; i++) { |
| 350 | + writer.write( |
| 351 | + GenericRowData.of(i, StringData.fromString("name" + i)), |
| 352 | + new MockSinkWriterContext()); |
| 353 | + } |
| 354 | + writer.flush(false); |
| 355 | + |
| 356 | + // Step 5: Verify data is written to 4 buckets |
| 357 | + Map<Integer, Integer> newBucketCounts = countRecordsPerBucket(testTablePath, 4); |
| 358 | + assertThat(newBucketCounts.size()).isEqualTo(4); |
| 359 | + int newTotalRecords = |
| 360 | + newBucketCounts.values().stream().mapToInt(Integer::intValue).sum(); |
| 361 | + assertThat(newTotalRecords).isEqualTo(20); // Total records from both writes |
| 362 | + |
| 363 | + // Verify that we have records in all 4 buckets |
| 364 | + Set<Integer> bucketsWithData = newBucketCounts.keySet(); |
| 365 | + assertThat(bucketsWithData).hasSize(4); |
| 366 | + for (int bucket = 0; bucket < 4; bucket++) { |
| 367 | + assertThat(bucketsWithData).contains(bucket); |
| 368 | + } |
| 369 | + } finally { |
| 370 | + writer.close(); |
| 371 | + } |
| 372 | + } |
| 373 | + |
| 374 | + private Map<Integer, Integer> countRecordsPerBucket(TablePath tablePath, int expectedBuckets) |
| 375 | + throws Exception { |
| 376 | + Map<Integer, Integer> bucketCounts = new HashMap<>(); |
| 377 | + Configuration clientConfig = FLUSS_CLUSTER_EXTENSION.getClientConfig(); |
| 378 | + try (Connection connection = ConnectionFactory.createConnection(clientConfig); |
| 379 | + Table table = connection.getTable(tablePath); |
| 380 | + LogScanner logScanner = table.newScan().createLogScanner()) { |
| 381 | + // Subscribe to all buckets from beginning |
| 382 | + for (int bucket = 0; bucket < expectedBuckets; bucket++) { |
| 383 | + logScanner.subscribeFromBeginning(bucket); |
| 384 | + } |
| 385 | + |
| 386 | + // Collect all records and count by bucket |
| 387 | + int totalScanned = 0; |
| 388 | + int maxRecords = 50; // Limit to avoid infinite loop |
| 389 | + while (totalScanned < maxRecords) { |
| 390 | + org.apache.fluss.client.table.scanner.log.ScanRecords scanRecords = |
| 391 | + logScanner.poll(Duration.ofSeconds(1)); |
| 392 | + if (scanRecords.isEmpty()) { |
| 393 | + break; |
| 394 | + } |
| 395 | + for (TableBucket tableBucket : scanRecords.buckets()) { |
| 396 | + int bucketId = tableBucket.getBucket(); |
| 397 | + int recordCount = scanRecords.records(tableBucket).size(); |
| 398 | + bucketCounts.put( |
| 399 | + bucketId, bucketCounts.getOrDefault(bucketId, 0) + recordCount); |
| 400 | + totalScanned += recordCount; |
| 401 | + } |
| 402 | + } |
| 403 | + } |
| 404 | + return bucketCounts; |
| 405 | + } |
| 406 | + |
268 | 407 | static class MockSinkWriterContext implements SinkWriter.Context { |
269 | 408 | @Override |
270 | 409 | public long currentWatermark() { |
|
0 commit comments