/* * (C) Copyright 2017 Nuxeo (http://nuxeo.com/) and others. * * 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. * * Contributors: * Florent Guillaume */ package org.nuxeo.ecm.core.pubsub; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.runtime.api.Framework; /** * Encapsulates invalidations management through the {@link PubSubService}. * <p> * All nodes that use the same topic will share the same invalidations. * <p> * The discriminator is used to distinguish nodes between one another, and to avoid that a node receives the * invalidations it send itself. * * @since 9.1 */ public abstract class AbstractPubSubInvalidator<T extends SerializableInvalidations> { private static final Log log = LogFactory.getLog(AbstractPubSubInvalidator.class); private static final String UTF_8 = "UTF-8"; protected String topic; protected byte[] discriminatorBytes; protected volatile T bufferedInvalidations; /** Constructs new empty invalidations, of type {@link T}. */ public abstract T newInvalidations(); /** Deserializes an {@link InputStream} into invalidations, or {@code null}. */ public abstract T deserialize(InputStream in) throws IOException; /** * Initializes the invalidator. * * @param topic the topic * @param discriminator the discriminator */ public void initialize(String topic, String discriminator) { this.topic = topic; try { discriminatorBytes = discriminator.getBytes(UTF_8); } catch (UnsupportedEncodingException e) { throw new IllegalArgumentException(e); } for (byte b : discriminatorBytes) { if (b == DISCRIMINATOR_SEP) { throw new IllegalArgumentException("Invalid discriminator, must not contains separator '" + (char) DISCRIMINATOR_SEP + "': " + discriminator); } } bufferedInvalidations = newInvalidations(); PubSubService pubSubService = Framework.getService(PubSubService.class); pubSubService.registerSubscriber(topic, this::subscriber); } /** * Closes this invalidator and releases resources. */ public void close() { PubSubService pubSubService = Framework.getService(PubSubService.class); pubSubService.unregisterSubscriber(topic, this::subscriber); // not null to avoid crashing subscriber thread still in flight bufferedInvalidations = newInvalidations(); } protected static final byte DISCRIMINATOR_SEP = ':'; /** * Sends invalidations to other nodes. */ public void sendInvalidations(T invalidations) { if (log.isTraceEnabled()) { log.trace("Sending invalidations: " + invalidations); } ByteArrayOutputStream baout = new ByteArrayOutputStream(); try { baout.write(discriminatorBytes); } catch (IOException e) { // cannot happen, ByteArrayOutputStream.write doesn't throw return; } baout.write(DISCRIMINATOR_SEP); try { invalidations.serialize(baout); } catch (IOException e) { log.error("Failed to serialize invalidations", e); // don't crash for this return; } byte[] message = baout.toByteArray(); PubSubService pubSubService = Framework.getService(PubSubService.class); pubSubService.publish(topic, message); } /** * PubSubService subscriber, called from a separate thread. */ protected void subscriber(String topic, byte[] message) { int start = scanDiscriminator(message); if (start == -1) { // same discriminator or invalid message return; } InputStream bain = new ByteArrayInputStream(message, start, message.length - start); T invalidations; try { invalidations = deserialize(bain); } catch (IOException e) { log.error("Failed to deserialize invalidations", e); // don't crash for this return; } if (invalidations == null || invalidations.isEmpty()) { return; } if (log.isTraceEnabled()) { log.trace("Receiving invalidations: " + invalidations); } synchronized (this) { bufferedInvalidations.add(invalidations); } } /** * Scans for the discriminator and returns the payload start offset. * * @return payload start offset, or -1 if the discriminator is local or if the message is invalid */ protected int scanDiscriminator(byte[] message) { if (message == null) { return -1; } int start = -1; boolean differ = false; for (int i = 0; i < message.length; i++) { byte b = message[i]; if (b == DISCRIMINATOR_SEP) { differ = differ || discriminatorBytes.length > i; start = i + 1; break; } if (!differ) { if (i == discriminatorBytes.length) { // discriminator is a prefix of the received one differ = true; } else if (b != discriminatorBytes[i]) { // difference differ = true; } } } if (!differ) { // same discriminator return -1; } return start; // may be -1 if separator was never found (invalid message) } /** * Receives invalidations from other nodes. */ public T receiveInvalidations() { T newInvalidations = newInvalidations(); T invalidations; synchronized (this) { invalidations = bufferedInvalidations; bufferedInvalidations = newInvalidations; } if (log.isTraceEnabled()) { log.trace("Received invalidations: " + invalidations); } return invalidations; } }