/*
* (C) Copyright 2015 Nuxeo SA (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:
* Benoit Delbosc
*/
package org.nuxeo.ecm.core.redis.contribs;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.core.model.Repository;
import org.nuxeo.ecm.core.redis.RedisAdmin;
import org.nuxeo.ecm.core.redis.RedisExecutor;
import org.nuxeo.ecm.core.storage.dbs.DBSClusterInvalidator;
import org.nuxeo.ecm.core.storage.dbs.DBSInvalidations;
import org.nuxeo.runtime.api.Framework;
import redis.clients.jedis.JedisPubSub;
/**
* Redis implementation of {@link DBSClusterInvalidator}. Use a single channel pubsub to send invalidations. Use an HSET
* to register nodes, only for debug purpose so far.
*
* @since 8.10
* @deprecated since 9.1, use DBSPubSubInvalidator instead
*/
@Deprecated
public class RedisDBSClusterInvalidator implements DBSClusterInvalidator {
protected static final String PREFIX = "inval";
// PubSub channel: nuxeo:inval:<repositoryName>:channel
protected static final String INVALIDATION_CHANNEL = "channel";
// Node HSET key: nuxeo:inval:<repositoryName>:nodes:<nodeId>
protected static final String CLUSTER_NODES_KEY = "nodes";
// Keep info about a cluster node for one day
protected static final int TIMEOUT_REGISTER_SECOND = 24 * 3600;
// Max delay to wait for a channel subscription
protected static final long TIMEOUT_SUBSCRIBE_SECOND = 10;
protected static final String STARTED_FIELD = "started";
protected static final String LAST_INVAL_FIELD = "lastInvalSent";
protected String nodeId;
protected String repositoryName;
protected RedisExecutor redisExecutor;
protected DBSInvalidations receivedInvals;
protected Thread subscriberThread;
protected String namespace;
protected String startedDateTime;
private static final Log log = LogFactory.getLog(RedisDBSClusterInvalidator.class);
private CountDownLatch subscribeLatch;
private String registerSha;
private String sendSha;
@Override
public void initialize(String nodeId, String repositoryName) {
this.nodeId = nodeId;
this.repositoryName = repositoryName;
redisExecutor = Framework.getService(RedisExecutor.class);
RedisAdmin redisAdmin = Framework.getService(RedisAdmin.class);
namespace = redisAdmin.namespace(PREFIX, repositoryName);
try {
registerSha = redisAdmin.load("org.nuxeo.ecm.core.redis", "register-node-inval");
sendSha = redisAdmin.load("org.nuxeo.ecm.core.redis", "send-inval");
} catch (IOException e) {
throw new RuntimeException(e);
}
receivedInvals = new DBSInvalidations();
createSubscriberThread();
registerNode();
}
protected void createSubscriberThread() {
subscribeLatch = new CountDownLatch(1);
String name = "RedisDBSClusterInvalidatorSubscriber:" + repositoryName + ":" + nodeId;
subscriberThread = new Thread(this::subscribeToInvalidationChannel, name);
subscriberThread.setUncaughtExceptionHandler((t, e) -> log.error("Uncaught error on thread " + t.getName(), e));
subscriberThread.setPriority(Thread.NORM_PRIORITY);
subscriberThread.start();
try {
if (!subscribeLatch.await(TIMEOUT_SUBSCRIBE_SECOND, TimeUnit.SECONDS)) {
log.error("Redis channel subscription timeout after " + TIMEOUT_SUBSCRIBE_SECOND
+ "s, continuing but this node may not receive cluster invalidations");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}
protected void subscribeToInvalidationChannel() {
log.info("Subscribing to channel: " + getChannelName());
redisExecutor.subscribe(new JedisPubSub() {
@Override
public void onSubscribe(String channel, int subscribedChannels) {
super.onSubscribe(channel, subscribedChannels);
if (subscribeLatch != null) {
subscribeLatch.countDown();
}
if (log.isDebugEnabled()) {
log.debug("Subscribed to channel: " + getChannelName());
}
}
@Override
public void onMessage(String channel, String message) {
try {
RedisDBSInvalidations rInvals = new RedisDBSInvalidations(nodeId, message);
if (log.isTraceEnabled()) {
log.trace("Receive invalidations: " + rInvals);
}
DBSInvalidations invals = rInvals.getInvalidations();
synchronized (RedisDBSClusterInvalidator.this) {
receivedInvals.add(invals);
}
} catch (IllegalArgumentException e) {
log.error("Fail to read message: " + message, e);
}
}
}, getChannelName());
}
protected String getChannelName() {
return namespace + INVALIDATION_CHANNEL;
}
protected void registerNode() {
startedDateTime = getCurrentDateTime();
List<String> keys = Collections.singletonList(getNodeKey());
List<String> args = Arrays.asList(STARTED_FIELD, startedDateTime,
Integer.valueOf(TIMEOUT_REGISTER_SECOND).toString());
if (log.isDebugEnabled()) {
log.debug("Registering node: " + nodeId);
}
redisExecutor.evalsha(registerSha, keys, args);
if (log.isInfoEnabled()) {
log.info("Node registered: " + nodeId);
}
}
protected String getNodeKey() {
return namespace + CLUSTER_NODES_KEY + ":" + nodeId;
}
@Override
public void close() {
log.debug("Closing");
unsubscribeToInvalidationChannel();
// The Jedis pool is already closed when the repository is shutdowned
receivedInvals.clear();
}
protected void unsubscribeToInvalidationChannel() {
subscriberThread.interrupt();
}
@Override
public DBSInvalidations receiveInvalidations() {
DBSInvalidations newInvals = new DBSInvalidations();
DBSInvalidations ret;
synchronized (this) {
ret = receivedInvals;
receivedInvals = newInvals;
}
return ret;
}
@Override
public void sendInvalidations(DBSInvalidations invals) {
RedisDBSInvalidations rInvals = new RedisDBSInvalidations(nodeId, invals);
if (log.isTraceEnabled()) {
log.trace("Sending invalidations: " + rInvals);
}
List<String> keys = Arrays.asList(getChannelName(), getNodeKey());
List<String> args = Arrays.asList(rInvals.serialize(), STARTED_FIELD, startedDateTime, LAST_INVAL_FIELD,
getCurrentDateTime(), Integer.valueOf(TIMEOUT_REGISTER_SECOND).toString());
redisExecutor.evalsha(sendSha, keys, args);
log.trace("invals sent");
}
protected String getCurrentDateTime() {
return LocalDateTime.now().toString();
}
}