/** * Attempts to install a new transactions table schema version, by submitting a relevant transform. * * The execution of this method does not guarantee that the provided version will eventually be installed. * This method returns true if and only if in the map agreed by the coordination service evaluated at the validity * bound, the transactions schema version is equal to newVersion. */ public boolean tryInstallNewTransactionsSchemaVersion(int newVersion) { List<Integer> presentVersionPeakValidity = coordinationService.tryTransformCurrentValue( valueAndBound -> installNewVersionInMapOrDefault(newVersion, valueAndBound)) .existingValues() .stream() .map(valueAndBound -> valueAndBound.value() .orElseThrow(() -> new SafeIllegalStateException("Unexpectedly found no value in store")) .timestampToTransactionsTableSchemaVersion() .getValueForTimestamp(valueAndBound.bound())) .collect(Collectors.toList()); return Iterables.getOnlyElement(presentVersionPeakValidity) == newVersion; }
private static <T> ValueAndBound<T> getInitialCacheValue() { return ValueAndBound.of(Optional.empty(), ValueAndBound.INVALID_BOUND); } }
private ValueAndBound<T> chooseValueWithGreaterBound( ValueAndBound<T> currentValue, ValueAndBound<T> nextValue) { if (currentValue.bound() > nextValue.bound()) { return currentValue; } log.info("Updating cached coordination value to a new value, valid till {}", SafeArg.of("newBound", nextValue.bound())); return nextValue; }
private CheckAndSetResult<ValueAndBound<InternalSchemaMetadata>> tryPerpetuateExistingState() { return coordinationService.tryTransformCurrentValue(valueAndBound -> valueAndBound.value().orElseGet(InternalSchemaMetadata::defaultValue)); }
private CheckAndSetResult<ValueAndBound<T>> extractRelevantValues(T targetValue, long newBound, CheckAndSetResult<SequenceAndBound> casResult) { if (casResult.successful()) { return CheckAndSetResult.of(true, ImmutableList.of(ValueAndBound.of(Optional.of(targetValue), newBound))); } return CheckAndSetResult.of( false, casResult.existingValues() .stream() .map(value -> ValueAndBound.of(getValue(value.sequence()), value.bound())) .collect(Collectors.toList())); }
@Override public Optional<ValueAndBound<T>> getValueForTimestamp(long timestamp) { ValueAndBound<T> cachedReference = cache.get(); if (cachedReference.bound() < timestamp) { return readLatestValueFromStore() .filter(valueAndBound -> valueAndBound.bound() >= timestamp); } return Optional.of(cachedReference); }
@SuppressWarnings("OptionalUsedAsFieldOrParameterType") // Passed from other operations returning Optional private SequenceAndBound determineNewSequenceAndBound( Optional<SequenceAndBound> coordinationValue, ValueAndBound<T> extantValueAndBound, T targetValue) { long sequenceNumber; long newBound; if (shouldReuseExtantValue(coordinationValue, extantValueAndBound.value(), targetValue)) { // Safe as we're only on this branch if the value is present sequenceNumber = coordinationValue.get().sequence(); newBound = getNewBound(sequenceNumberSupplier.getAsLong()); } else { sequenceNumber = sequenceNumberSupplier.getAsLong(); putUnlessValueExists(sequenceNumber, targetValue); newBound = getNewBound(sequenceNumber); } return SequenceAndBound.of(sequenceNumber, newBound); }
private InternalSchemaMetadata installNewVersionInMapOrDefault(int newVersion, ValueAndBound<InternalSchemaMetadata> valueAndBound) { if (!valueAndBound.value().isPresent()) { log.warn("Attempting to install a new transactions schema version {}, but no past data was found," + " so we attempt to install default instead. This should normally only happen once per" + " server, and only on or around first startup since upgrading to a version of AtlasDB" + " that is aware of the transactions table. If this message persists, please contact" + " support.", SafeArg.of("newVersion", newVersion)); return InternalSchemaMetadata.defaultValue(); } log.info("Attempting to install a new transactions schema version {}, on top of schema metadata" + " that is valid up till timestamp {}.", SafeArg.of("newVersion", newVersion), SafeArg.of("oldDataValidity", valueAndBound.bound())); InternalSchemaMetadata internalSchemaMetadata = valueAndBound.value().get(); return InternalSchemaMetadata.builder() .from(internalSchemaMetadata) .timestampToTransactionsTableSchemaVersion( installNewVersionInMap( internalSchemaMetadata.timestampToTransactionsTableSchemaVersion(), valueAndBound.bound() + 1, newVersion)) .build(); }
@Override public Optional<ValueAndBound<T>> getAgreedValue() { return getCoordinationValue() .map(sequenceAndBound -> ValueAndBound.of( getValue(sequenceAndBound.sequence()), sequenceAndBound.bound())); }
/** * Registers a gauge which tracks the eventual transactions schema version - that is, at the end of the current * period of validity for the bound, what the metadata says the transactions schema version should be. * * @param metricsManager metrics manager to register the gauge on * @param metadataCoordinationService metadata coordination service that should be tracked */ private static void registerEventualTransactionsSchemaVersionMetric(MetricsManager metricsManager, CoordinationService<InternalSchemaMetadata> metadataCoordinationService) { metricsManager.registerMetric( MetadataCoordinationServiceMetrics.class, AtlasDbMetricNames.COORDINATION_EVENTUAL_TRANSACTIONS_SCHEMA_VERSION, TrackerUtils.createCachingExceptionHandlingGauge( log, Clock.defaultClock(), AtlasDbMetricNames.COORDINATION_EVENTUAL_TRANSACTIONS_SCHEMA_VERSION, () -> { Optional<ValueAndBound<InternalSchemaMetadata>> latestValue = metadataCoordinationService.getLastKnownLocalValue(); return latestValue .map(ValueAndBound::value) .flatMap(Function.identity()) .map(InternalSchemaMetadata::timestampToTransactionsTableSchemaVersion) .map(timestampMap -> timestampMap.getValueForTimestamp(latestValue.get().bound())) .orElse(null); })); }
@Test public void multipleStoresCanCoexist() { byte[] otherCoordinationKey = PtBytes.toBytes("bbbbb"); CoordinationStore<String> otherCoordinationStore = KeyValueServiceCoordinationStore.create( ObjectMappers.newServerObjectMapper(), keyValueService, otherCoordinationKey, timestampSequence::incrementAndGet, String.class, false); coordinationStore.transformAgreedValue(unused -> VALUE_1); otherCoordinationStore.transformAgreedValue(unused -> VALUE_2); assertThat(coordinationStore.getAgreedValue().get().value()).contains(VALUE_1); assertThat(otherCoordinationStore.getAgreedValue().get().value()).contains(VALUE_2); } }
@Test public void valuePreservingTransformationsAdvanceTheBound() { coordinationStore.transformAgreedValue(unused -> VALUE_1); ValueAndBound<String> firstValueAndBound = coordinationStore.getAgreedValue().get(); coordinationStore.transformAgreedValue(VALUE_PRESERVING_FUNCTION); ValueAndBound<String> secondValueAndBound = coordinationStore.getAgreedValue().get(); assertThat(firstValueAndBound.value()).contains(VALUE_1); assertThat(secondValueAndBound.value()).contains(VALUE_1); assertThat(firstValueAndBound.bound()).isLessThan(secondValueAndBound.bound()); }
@Override public CheckAndSetResult<ValueAndBound<T>> transformAgreedValue(Function<ValueAndBound<T>, T> transform) { Optional<SequenceAndBound> coordinationValue = getCoordinationValue(); ValueAndBound<T> extantValueAndBound = ValueAndBound.of(coordinationValue.flatMap( sequenceAndBound -> getValue(sequenceAndBound.sequence())), coordinationValue.map(SequenceAndBound::bound).orElse(SequenceAndBound.INVALID_BOUND)); T targetValue = transform.apply(extantValueAndBound); SequenceAndBound newSequenceAndBound = determineNewSequenceAndBound(coordinationValue, extantValueAndBound, targetValue); CheckAndSetResult<SequenceAndBound> casResult = checkAndSetCoordinationValue( coordinationValue, newSequenceAndBound); return extractRelevantValues(targetValue, newSequenceAndBound.bound(), casResult); }
/** * Returns the version of the transactions schema associated with the provided timestamp. * * This method may perpetuate the existing state one or more times to achieve consensus. It will repeatedly * attempt to perpetuate the existing state until a consensus for the provided timestamp argument is achieved. * * This method should only be called with timestamps that have already been given out by the timestamp service; * otherwise, achieving a consensus may take a long time. */ public int getTransactionsSchemaVersion(long timestamp) { if (timestamp < AtlasDbConstants.STARTING_TS) { throw new SafeIllegalStateException("Query attempted for timestamp {} which was never given out by the" + " timestamp service, as timestamps start at {}", SafeArg.of("queriedTimestamp", timestamp), SafeArg.of("startOfTime", AtlasDbConstants.STARTING_TS)); } Optional<Integer> possibleVersion = extractTimestampVersion(coordinationService.getValueForTimestamp(timestamp), timestamp); while (!possibleVersion.isPresent()) { CheckAndSetResult<ValueAndBound<InternalSchemaMetadata>> casResult = tryPerpetuateExistingState(); possibleVersion = extractTimestampVersion(casResult.existingValues() .stream() .filter(valueAndBound -> valueAndBound.bound() >= timestamp) .findAny(), timestamp); } return possibleVersion.get(); }
private CheckAndSetResult<ValueAndBound<InternalSchemaMetadata>> tryPerpetuateExistingState() { return coordinationService.tryTransformCurrentValue(valueAndBound -> valueAndBound.value().orElseGet(InternalSchemaMetadata::defaultValue)); }
@Test public void canApplyMultipleTransformations() { coordinationStore.transformAgreedValue(unused -> VALUE_1); ValueAndBound<String> firstValueAndBound = coordinationStore.getAgreedValue().get(); coordinationStore.transformAgreedValue(unused -> VALUE_2); ValueAndBound<String> secondValueAndBound = coordinationStore.getAgreedValue().get(); assertThat(firstValueAndBound.value()).contains(VALUE_1); assertThat(secondValueAndBound.value()).contains(VALUE_2); assertThat(firstValueAndBound.bound()).isLessThan(secondValueAndBound.bound()); }
@Test public void getsLastKnownLocalValueFromDelegateIfPresent() { when(delegate.getLastKnownLocalValue()).thenReturn(Optional.of(ValueAndBound.of(INTEGER_1, BOUND))); assertThat(coordinationService.getLastKnownLocalValue()).contains(ValueAndBound.of(STRING_1, BOUND)); verify(delegate).getLastKnownLocalValue(); verify(intToStringTransform).apply(INTEGER_1); }
private ValueAndBound<T> chooseValueWithGreaterBound( ValueAndBound<T> currentValue, ValueAndBound<T> nextValue) { if (currentValue.bound() > nextValue.bound()) { return currentValue; } log.info("Updating cached coordination value to a new value, valid till {}", SafeArg.of("newBound", nextValue.bound())); return nextValue; }