/* * ModeShape (http://www.modeshape.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.modeshape.jcr.journal; import java.io.Serializable; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.modeshape.schematic.annotation.ThreadSafe; import org.modeshape.common.logging.Logger; import org.modeshape.common.util.CheckArg; import org.modeshape.common.util.DateTimeUtil; import org.modeshape.jcr.JcrI18n; import org.modeshape.jcr.cache.NodeKey; import org.modeshape.jcr.cache.change.ChangeSet; import org.modeshape.jcr.clustering.ClusteringService; import org.modeshape.jcr.clustering.MessageConsumer; /** * A {@link ChangeJournal} implementation which runs in a cluster and which attempts to reconcile with other members of the cluster * on startup in order to retrieve missed/lost records. * * @author Horia Chiorean (hchiorea@redhat.com) */ @ThreadSafe public class ClusteredJournal extends MessageConsumer<ClusteredJournal.DeltaMessage> implements ChangeJournal { private final static Logger LOGGER = Logger.getLogger(ClusteredJournal.class); private final static int MAX_MINUTES_TO_WAIT_FOR_RECONCILIATION = 2; private final LocalJournal localJournal; private final ClusteringService clusteringService; private final int reconciliationMaxWaitTimeMinutes; private CountDownLatch reconciliationLatch = null; /** * Creates a new clustered journal * * @param localJournal the local {@link ChangeJournal} which will * @param clusteringService an {@link ClusteringService} instance. */ public ClusteredJournal( LocalJournal localJournal, ClusteringService clusteringService ) { this(localJournal, clusteringService, MAX_MINUTES_TO_WAIT_FOR_RECONCILIATION); } protected ClusteredJournal(LocalJournal localJournal, ClusteringService clusteringService, int reconciliationMaxWaitTime) { super(DeltaMessage.class); CheckArg.isNotNull(localJournal, "localJournal"); CheckArg.isNotNull(clusteringService, "clusteringService"); this.clusteringService = clusteringService; this.localJournal = localJournal.withSearchTimeDelta(clusteringService.getMaxAllowedClockDelayMillis()); this.reconciliationMaxWaitTimeMinutes = reconciliationMaxWaitTime; } @Override public void notify( ChangeSet changeSet ) { localJournal.notify(changeSet); } @Override public void start() throws Exception { // make sure the clustering service is open if (!clusteringService.isOpen()) { throw new IllegalStateException("The clustering service has not been started"); } localJournal.start(); //make sure this process can always process delta messages clusteringService.addConsumer(this); if (!clusteringService.multipleMembersInCluster()) { // this is the first node of the cluster, nothing to do return; } // we require just 1 response before unblocking for a couple of reasons: // a) partition tolerance is NOT SUPPORTED // b) each member of the cluster has a full view of all the changes throughout that cluster, thanks to remote events // we'll process all responses eventually, but we only block for the first one int numberOfRequiredResponses = 1; this.reconciliationLatch = new CountDownLatch(numberOfRequiredResponses); // send the request JournalRecord lastRecord = lastRecord(); Long lastChangeSetTimeMillis = lastRecord != null ? lastRecord.getChangeTimeMillis() : null; DeltaMessage request = DeltaMessage.request(journalId(), lastChangeSetTimeMillis); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Sending delta request: {0}", request); } this.clusteringService.sendMessage(request); waitForReconciliationToComplete(); } private void waitForReconciliationToComplete() throws InterruptedException { try { if (LOGGER.isDebugEnabled()) { LOGGER.debug("{0} waiting until it receives {1} responses from cluster {2}", journalId(), reconciliationLatch.getCount(), clusterName()); } if (!reconciliationLatch.await(reconciliationMaxWaitTimeMinutes, TimeUnit.MINUTES)) { LOGGER.warn(JcrI18n.journalHasNotCompletedReconciliation,journalId(), clusterName(), reconciliationMaxWaitTimeMinutes); reconciliationLatch.countDown(); } else if (LOGGER.isDebugEnabled()) { LOGGER.debug("{0} successfully completed reconciliation", journalId()); } } catch (InterruptedException e) { LOGGER.warn(JcrI18n.journalHasNotCompletedReconciliation, journalId(), clusterName(), MAX_MINUTES_TO_WAIT_FOR_RECONCILIATION); if (Thread.interrupted()) { throw e; } } } @Override public void shutdown() { localJournal.shutdown(); } @Override public void removeOldRecords() { localJournal.removeOldRecords(); } @Override public Records allRecords( boolean descendingOrder ) { return localJournal.allRecords(descendingOrder); } @Override public JournalRecord lastRecord() { return localJournal.lastRecord(); } @Override public Records recordsNewerThan( LocalDateTime changeSetTime, boolean inclusive, boolean descendingOrder ) { return localJournal.recordsNewerThan(changeSetTime, inclusive, descendingOrder); } @Override public Iterator<NodeKey> changedNodesSince( long timestamp ) { return localJournal.changedNodesSince(timestamp); } @Override public void addRecords( JournalRecord... records ) { localJournal.addRecords(records); } @Override public String journalId() { return localJournal.journalId(); } @Override public boolean started() { return localJournal.started() && reconciliationCompleted(); } @Override public void consume( ClusteredJournal.DeltaMessage message ) { if (!localJournal.started()) { return; } if (message.isResponse()) { processDeltaResponse(message); } else { processDeltaRequest(message); } } protected boolean reconciliationCompleted() { return reconciliationLatch == null || reconciliationLatch.getCount() == 0; } private void processDeltaRequest(DeltaMessage request) { String requestorId = request.getRequestorId(); String journalId = journalId(); if (requestorId.equals(journalId)) { //we MUST discard own messages, because JGroups will broadcast these as well LOGGER.debug("{0} discarding delta request from itself", journalId); return; } if (!reconciliationCompleted()) { //if this clustered journal has not completed reconciling itself, it cannot send anything LOGGER.debug("{0} is still reconciling, cannot send delta to journal {1}", journalId, requestorId); return; } Long requestorLastChangeSetTime = request.getRequestorLastChangeSetTime(); LocalDateTime lastChangeSetTime = requestorLastChangeSetTime != null ? DateTimeUtil.localDateTimeUTC(requestorLastChangeSetTime) : null; Records delta = recordsNewerThan(lastChangeSetTime, false, false); List<JournalRecord> deltaList = new ArrayList<>(delta.size()); for (JournalRecord record : delta) { deltaList.add(record); } DeltaMessage response = DeltaMessage.response(request, journalId, deltaList); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Sending delta response {0} to journal {1}", response, requestorId); } clusteringService.sendMessage(response); } private void processDeltaResponse(DeltaMessage message) { String journalId = journalId(); if (!journalId.equals(message.getRequestorId())) { // only process a response if the message is a response to our request (in a cluster everything will be broadcasted to everyone) return; } if (LOGGER.isDebugEnabled()) { LOGGER.debug("{0} received delta response {1}", journalId, message); } List<JournalRecord> records = message.getRespondentRecords(); if (!records.isEmpty()) { //make sure that a new timestamp is not generated for those records and whatever comes in the response is used. localJournal.addRecords(records.toArray(new JournalRecord[0])); } reconciliationLatch.countDown(); } protected ClusteringService clusteringService() { return ClusteredJournal.this.clusteringService; } protected LocalJournal localJournal() { return localJournal; } protected String clusterName() { return clusteringService().clusterName(); } protected static class DeltaMessage implements Serializable { private static final long serialVersionUID = 1L; private final String requestorId; private final Long requestorLastChangeSetTime; private final String respondentId; private final List<JournalRecord> respondentRecords; private DeltaMessage( String requestorId, Long requestorLastChangeSetTime, String respondentId, List<JournalRecord> respondentRecords ) { this.requestorId = requestorId; this.requestorLastChangeSetTime = requestorLastChangeSetTime; this.respondentId = respondentId; this.respondentRecords = respondentRecords; } protected boolean isResponse() { return this.respondentId != null; } protected String getRequestorId() { return requestorId; } protected Long getRequestorLastChangeSetTime() { return requestorLastChangeSetTime; } protected String getRespondentId() { return respondentId; } protected List<JournalRecord> getRespondentRecords() { return respondentRecords; } protected static DeltaMessage request(String requestorId, Long requestorLastChangeSetTime) { return new DeltaMessage(requestorId, requestorLastChangeSetTime, null, null); } protected static DeltaMessage response(DeltaMessage request, String repondentId, List<JournalRecord> respondentRecords) { return new DeltaMessage(request.requestorId, request.requestorLastChangeSetTime, repondentId, respondentRecords); } @Override public String toString() { StringBuilder sb = null; if (isResponse()) { sb = new StringBuilder("response["); sb.append("requestorId='").append(requestorId).append('\''); sb.append(", requestorLastChangeSetTime=").append(requestorLastChangeSetTime); sb.append(", repondentId='").append(respondentId).append('\''); sb.append(", respondentRecords=").append(respondentRecords); } else { sb = new StringBuilder("request["); sb.append("requestorId='").append(requestorId).append('\''); sb.append(", requestorLastChangeSetTime=").append(requestorLastChangeSetTime); } sb.append(']'); return sb.toString(); } } }