+
+ @Override
+ void onTransactionPurged(final TransactionIdentifier txId) {
+ final var history = getHistory(txId);
+ if (history != null) {
+ history.onTransactionPurged(txId);
+ LOG.debug("{}: Purged transaction {}", shardName(), txId);
+ } else {
+ LOG.warn("{}: Unknown history for purged transaction {}, ignoring", shardName(), txId);
+ }
+ }
+
+ @Override
+ void onTransactionsSkipped(final LocalHistoryIdentifier historyId, final ImmutableUnsignedLongSet txIds) {
+ final FrontendHistoryMetadataBuilder history = getHistory(historyId);
+ if (history != null) {
+ history.onTransactionsSkipped(txIds);
+ LOG.debug("{}: History {} skipped transactions {}", shardName(), historyId, txIds);
+ } else {
+ LOG.warn("{}: Unknown history {} for skipped transactions, ignoring", shardName(), historyId);
+ }
+ }
+
+ @Override
+ LeaderFrontendState toLeaderState(final Shard shard) {
+ // Note: we have to make sure to *copy* all current state and not leak any views, otherwise leader/follower
+ // interactions would get intertwined leading to inconsistencies.
+ final var histories = new HashMap<LocalHistoryIdentifier, LocalFrontendHistory>();
+ for (var historyMetaBuilder : currentHistories.values()) {
+ final var historyId = historyMetaBuilder.getIdentifier();
+ if (historyId.getHistoryId() != 0) {
+ final var state = historyMetaBuilder.toLeaderState(shard);
+ if (state instanceof LocalFrontendHistory localState) {
+ histories.put(historyId, localState);
+ } else {
+ throw new VerifyException("Unexpected state " + state);
+ }
+ }
+ }
+
+ final AbstractFrontendHistory singleHistory;
+ final var singleHistoryMeta = currentHistories.get(new LocalHistoryIdentifier(clientId(), 0));
+ if (singleHistoryMeta == null) {
+ final var tree = shard.getDataStore();
+ singleHistory = StandaloneFrontendHistory.create(shard.persistenceId(), clientId(), tree);
+ } else {
+ singleHistory = singleHistoryMeta.toLeaderState(shard);
+ }
+
+ return new LeaderFrontendState.Enabled(shard.persistenceId(), clientId(), shard.getDataStore(),
+ purgedHistories.mutableCopy(), singleHistory, histories);
+ }
+
+ @Override
+ ToStringHelper addToStringAttributes(final ToStringHelper helper) {
+ return super.addToStringAttributes(helper).add("current", currentHistories).add("purged", purgedHistories);
+ }
+
+ private FrontendHistoryMetadataBuilder getHistory(final TransactionIdentifier txId) {
+ return getHistory(txId.getHistoryId());
+ }
+
+ private FrontendHistoryMetadataBuilder getHistory(final LocalHistoryIdentifier historyId) {
+ final LocalHistoryIdentifier local;
+ if (historyId.getHistoryId() == 0 && historyId.getCookie() != 0) {
+ // We are pre-creating the history for free-standing transactions with a zero cookie, hence our lookup
+ // needs to account for that.
+ LOG.debug("{}: looking up {} instead of {}", shardName(), standaloneId, historyId);
+ local = standaloneId;
+ } else {
+ local = historyId;
+ }
+
+ return currentHistories.get(local);
+ }
+
+ private LocalHistoryIdentifier standaloneHistoryId() {
+ return new LocalHistoryIdentifier(clientId(), 0);
+ }
+ }
+
+ private static final Logger LOG = LoggerFactory.getLogger(FrontendClientMetadataBuilder.class);
+
+ private final @NonNull ClientIdentifier clientId;
+ private final @NonNull String shardName;
+
+ FrontendClientMetadataBuilder(final String shardName, final ClientIdentifier clientId) {
+ this.shardName = requireNonNull(shardName);
+ this.clientId = requireNonNull(clientId);
+ }
+
+ static FrontendClientMetadataBuilder of(final String shardName, final FrontendClientMetadata meta) {
+ // Completely empty histories imply disabled state, as otherwise we'd have a record of the single history --
+ // either purged or active
+ return meta.getCurrentHistories().isEmpty() && meta.getPurgedHistories().isEmpty()
+ ? new Disabled(shardName, meta.clientId()) : new Enabled(shardName, meta);
+ }
+
+ final ClientIdentifier clientId() {
+ return clientId;