|
31 | 31 | import com.google.common.annotations.VisibleForTesting; |
32 | 32 | import com.google.common.base.Preconditions; |
33 | 33 | import com.google.spanner.v1.BatchWriteResponse; |
| 34 | +import com.google.spanner.v1.TransactionOptions.IsolationLevel; |
| 35 | +import com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode; |
34 | 36 | import java.time.Clock; |
35 | 37 | import java.time.Duration; |
36 | 38 | import java.time.Instant; |
@@ -63,14 +65,30 @@ final class MultiplexedSessionDatabaseClient extends AbstractMultiplexedSessionD |
63 | 65 | */ |
64 | 66 | private static final int MAX_INITIAL_CREATE_SESSION_ATTEMPTS = 10; |
65 | 67 |
|
| 68 | + /** |
| 69 | + * Statement used to query database-level default options from |
| 70 | + * INFORMATION_SCHEMA.DATABASE_OPTIONS. This retrieves 'default_transaction_isolation', |
| 71 | + * 'default_read_lock_mode', and 'database_dialect' so that the client can correctly configure |
| 72 | + * transaction routing (e.g. Leader-Aware Routing) and isolation level behaviors without relying |
| 73 | + * solely on client-side hardcoded defaults. |
| 74 | + */ |
66 | 75 | @VisibleForTesting |
67 | | - static final Statement DETERMINE_DIALECT_STATEMENT = |
| 76 | + static final Statement DETERMINE_METADATA_STATEMENT = |
68 | 77 | Statement.newBuilder( |
69 | | - "select option_value " |
70 | | - + "from information_schema.database_options " |
71 | | - + "where option_name='database_dialect'") |
| 78 | + "SELECT OPTION_NAME, OPTION_VALUE " |
| 79 | + + "FROM INFORMATION_SCHEMA.DATABASE_OPTIONS " |
| 80 | + + "WHERE OPTION_NAME IN ('default_transaction_isolation', " |
| 81 | + + "'default_read_lock_mode', 'database_dialect')") |
72 | 82 | .build(); |
73 | 83 |
|
| 84 | + static final String OPTION_DATABASE_DIALECT = "database_dialect"; |
| 85 | + static final String OPTION_DEFAULT_TRANSACTION_ISOLATION = "default_transaction_isolation"; |
| 86 | + static final String OPTION_DEFAULT_READ_LOCK_MODE = "default_read_lock_mode"; |
| 87 | + |
| 88 | + static final String ISOLATION_LEVEL_REPEATABLE_READ = "repeatable read"; |
| 89 | + static final String READ_LOCK_MODE_OPTIMISTIC = "optimistic"; |
| 90 | + static final String READ_LOCK_MODE_PESSIMISTIC = "pessimistic"; |
| 91 | + |
74 | 92 | /** |
75 | 93 | * Represents a single transaction on a multiplexed session. This can be both a single-use or |
76 | 94 | * multi-use transaction, and both read/write or read-only transaction. This can be compared to a |
@@ -274,13 +292,8 @@ public void onSessionReady(SessionImpl session) { |
274 | 292 | // only start the maintainer if we actually managed to create a session in the first |
275 | 293 | // place. |
276 | 294 | maintainer.start(); |
277 | | - if (sessionClient |
278 | | - .getSpanner() |
279 | | - .getOptions() |
280 | | - .getSessionPoolOptions() |
281 | | - .isAutoDetectDialect()) { |
282 | | - MAINTAINER_SERVICE.submit(() -> getDialect()); |
283 | | - } |
| 295 | + MAINTAINER_SERVICE.submit( |
| 296 | + () -> session.getSessionReference().setDatabaseMetadata(getDatabaseMetadata())); |
284 | 297 | } |
285 | 298 |
|
286 | 299 | @Override |
@@ -371,6 +384,12 @@ AtomicLong getNumSessionsReleased() { |
371 | 384 | return this.numSessionsReleased; |
372 | 385 | } |
373 | 386 |
|
| 387 | + @VisibleForTesting |
| 388 | + void resetAcquiredAndReleasedCounts() { |
| 389 | + this.numSessionsAcquired.set(0L); |
| 390 | + this.numSessionsReleased.set(0L); |
| 391 | + } |
| 392 | + |
374 | 393 | void close() { |
375 | 394 | boolean releaseChannelUsage = false; |
376 | 395 | synchronized (this) { |
@@ -473,32 +492,71 @@ private int getSingleUseChannelHint() { |
473 | 492 | } |
474 | 493 | } |
475 | 494 |
|
476 | | - private final AbstractLazyInitializer<Dialect> dialectSupplier = |
477 | | - new AbstractLazyInitializer<Dialect>() { |
| 495 | + private static IsolationLevel parseIsolationLevel(String value) { |
| 496 | + return ISOLATION_LEVEL_REPEATABLE_READ.equalsIgnoreCase(value) |
| 497 | + ? IsolationLevel.REPEATABLE_READ |
| 498 | + : IsolationLevel.SERIALIZABLE; |
| 499 | + } |
| 500 | + |
| 501 | + private static ReadLockMode parseReadLockMode(String value) { |
| 502 | + if (READ_LOCK_MODE_OPTIMISTIC.equalsIgnoreCase(value)) { |
| 503 | + return ReadLockMode.OPTIMISTIC; |
| 504 | + } else if (READ_LOCK_MODE_PESSIMISTIC.equalsIgnoreCase(value)) { |
| 505 | + return ReadLockMode.PESSIMISTIC; |
| 506 | + } |
| 507 | + return ReadLockMode.READ_LOCK_MODE_UNSPECIFIED; |
| 508 | + } |
| 509 | + |
| 510 | + /** |
| 511 | + * Lazily initializes and caches {@link DatabaseMetadata} (dialect, default isolation level, and |
| 512 | + * read lock mode). Introspects the database options once and attaches the resolved metadata to |
| 513 | + * the current multiplexed {@link SessionReference} so subsequent transactions can resolve their |
| 514 | + * effective modes. |
| 515 | + */ |
| 516 | + private final AbstractLazyInitializer<DatabaseMetadata> metadataSupplier = |
| 517 | + new AbstractLazyInitializer<DatabaseMetadata>() { |
478 | 518 | @Override |
479 | | - protected Dialect initialize() { |
480 | | - try (ResultSet dialectResultSet = singleUse().executeQuery(DETERMINE_DIALECT_STATEMENT)) { |
481 | | - if (dialectResultSet.next()) { |
482 | | - return Dialect.fromName(dialectResultSet.getString(0)); |
| 519 | + protected DatabaseMetadata initialize() { |
| 520 | + Dialect dialect = Dialect.GOOGLE_STANDARD_SQL; |
| 521 | + IsolationLevel isolationLevel = IsolationLevel.SERIALIZABLE; |
| 522 | + ReadLockMode readLockMode = ReadLockMode.READ_LOCK_MODE_UNSPECIFIED; |
| 523 | + |
| 524 | + numSessionsAcquired.decrementAndGet(); |
| 525 | + try (ResultSet resultSet = singleUse().executeQuery(DETERMINE_METADATA_STATEMENT)) { |
| 526 | + while (resultSet.next()) { |
| 527 | + String name = resultSet.getString(0); |
| 528 | + String value = resultSet.getString(1); |
| 529 | + if (OPTION_DATABASE_DIALECT.equalsIgnoreCase(name)) { |
| 530 | + dialect = Dialect.fromName(value); |
| 531 | + } else if (OPTION_DEFAULT_TRANSACTION_ISOLATION.equalsIgnoreCase(name)) { |
| 532 | + isolationLevel = parseIsolationLevel(value); |
| 533 | + } else if (OPTION_DEFAULT_READ_LOCK_MODE.equalsIgnoreCase(name)) { |
| 534 | + readLockMode = parseReadLockMode(value); |
| 535 | + } |
483 | 536 | } |
| 537 | + } finally { |
| 538 | + numSessionsReleased.decrementAndGet(); |
484 | 539 | } |
485 | | - // This should not really happen, but it is the safest fallback value. |
486 | | - return Dialect.GOOGLE_STANDARD_SQL; |
| 540 | + return new DatabaseMetadata(dialect, isolationLevel, readLockMode); |
487 | 541 | } |
488 | 542 | }; |
489 | 543 |
|
490 | | - @Override |
491 | | - public Dialect getDialect() { |
| 544 | + DatabaseMetadata getDatabaseMetadata() { |
492 | 545 | try { |
493 | | - return dialectSupplier.get(); |
| 546 | + return metadataSupplier.get(); |
494 | 547 | } catch (Exception exception) { |
495 | 548 | throw SpannerExceptionFactory.asSpannerException(exception); |
496 | 549 | } |
497 | 550 | } |
498 | 551 |
|
| 552 | + @Override |
| 553 | + public Dialect getDialect() { |
| 554 | + return getDatabaseMetadata().getDialect(); |
| 555 | + } |
| 556 | + |
499 | 557 | Future<Dialect> getDialectAsync() { |
500 | 558 | try { |
501 | | - return MAINTAINER_SERVICE.submit(dialectSupplier::get); |
| 559 | + return MAINTAINER_SERVICE.submit(() -> getDialect()); |
502 | 560 | } catch (Exception exception) { |
503 | 561 | throw SpannerExceptionFactory.asSpannerException(exception); |
504 | 562 | } |
@@ -659,8 +717,10 @@ void maintain() { |
659 | 717 | new SessionConsumer() { |
660 | 718 | @Override |
661 | 719 | public void onSessionReady(SessionImpl session) { |
662 | | - multiplexedSessionReference.set( |
663 | | - ApiFutures.immediateFuture(session.getSessionReference())); |
| 720 | + SessionReference sessionRef = session.getSessionReference(); |
| 721 | + multiplexedSessionReference.set(ApiFutures.immediateFuture(sessionRef)); |
| 722 | + MAINTAINER_SERVICE.submit( |
| 723 | + () -> sessionRef.setDatabaseMetadata(getDatabaseMetadata())); |
664 | 724 | expirationDate.set( |
665 | 725 | clock |
666 | 726 | .instant() |
|
0 commit comments