/** * Licensed to The Apereo Foundation under one or more contributor license * agreements. See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * * The Apereo Foundation licenses this file to you under the Educational * Community 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://opensource.org/licenses/ecl2.txt * * 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.opencastproject.message.broker.api.index; import static com.entwinemedia.fn.Stream.$; import static java.lang.String.format; import org.opencastproject.index.IndexProducer; import org.opencastproject.message.broker.api.BaseMessage; import org.opencastproject.message.broker.api.MessageReceiver; import org.opencastproject.message.broker.api.MessageSender; import org.opencastproject.security.api.DefaultOrganization; import org.opencastproject.security.api.Organization; import org.opencastproject.security.api.SecurityService; import org.opencastproject.security.api.User; import org.opencastproject.security.util.SecurityUtil; import org.opencastproject.util.RequireUtil; import org.opencastproject.util.data.Effect0; import com.entwinemedia.fn.P1; import com.entwinemedia.fn.Products; import com.entwinemedia.fn.data.Opt; import com.entwinemedia.fn.fns.Booleans; import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.text.WordUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.Serializable; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.FutureTask; /** * This service produces messages for an elastic search index */ public abstract class AbstractIndexProducer implements IndexProducer { public static final P1<Serializable> IDENTITY_MSG = Products.E.<Serializable>p1(new Serializable() { }); public abstract String getClassName(); public abstract MessageReceiver getMessageReceiver(); public abstract MessageSender getMessageSender(); public abstract SecurityService getSecurityService(); public abstract IndexRecreateObject.Service getService(); public abstract String getSystemUserName(); /** The message watcher */ private MessageWatcher messageWatcher; /** Single thread executor */ private final ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); /** * Initialize the index producer. */ public void activate() { messageWatcher = new MessageWatcher(); singleThreadExecutor.execute(messageWatcher); } /** * Clean-up resources at shutdown. */ public void deactivate() { if (messageWatcher != null) { messageWatcher.stopListening(); } singleThreadExecutor.shutdown(); } /* ------------------------------------------------------------------------------------------------------------------ */ /** * Create a new batch. * * @param indexName * the name of the index to recreate * @param queuePrefix * the message queue prefix where messages are sent to * @param updatesTotal * the number of updates that will be sent, i.e. how many times will the * {@link org.opencastproject.message.broker.api.index.AbstractIndexProducer.IndexRecreationBatch#update(Organization, P1[])} * method be called * @param endMessageOrg * the organization under which the batch's end message should be sent; * if none use the organization of the last update message */ public IndexRecreationBatch mkRecreationBatch(String indexName, String queuePrefix, int updatesTotal, Opt<Organization> endMessageOrg) { return new IndexRecreationBatch(indexName, queuePrefix, updatesTotal, endMessageOrg); } /** * Create a new batch. The organization under which the final end message is sent is set to {@link DefaultOrganization}. * * @param indexName * the name of the index to recreate * @param queuePrefix * the message queue prefix where messages are sent to * @param updatesTotal * the number of updates that will be sent, i.e. how many times will the * {@link org.opencastproject.message.broker.api.index.AbstractIndexProducer.IndexRecreationBatch#update(Organization, P1[])} * method be called * @see #mkRecreationBatch(String, String, int, Opt) */ public IndexRecreationBatch mkRecreationBatch(String indexName, String queuePrefix, int updatesTotal) { return new IndexRecreationBatch(indexName, queuePrefix, updatesTotal, Opt.<Organization>some(new DefaultOrganization())); } /** * State management for a batch of recreate index update messages. * Messages are always sent under the identity of the system user. */ public final class IndexRecreationBatch { private final Logger logger = LoggerFactory.getLogger(IndexRecreationBatch.class); private final String indexName; private final String destinationId; private final int updatesTotal; private final Opt<Organization> endMessageOrg; private int updatesCurrent; /** * Create a new batch. * * @param indexName * the name of the index to recreate * @param queuePrefix * the message queue prefix where messages are sent to * @param updatesTotal * the number of updates that will be sent, i.e. how many times will the {@link #update(Organization, P1[])} * method be called * @param endMessageOrg * the organization under which the batch's end message should be sent; * if none use the organization of the last update message */ private IndexRecreationBatch(String indexName, String queuePrefix, int updatesTotal, Opt<Organization> endMessageOrg) { this.indexName = indexName; this.destinationId = queuePrefix + WordUtils.capitalize(indexName); this.updatesTotal = RequireUtil.min(updatesTotal, 0); this.endMessageOrg = endMessageOrg; this.updatesCurrent = 0; } public int getUpdatesTotal() { return updatesTotal; } /** * Send one update to recreate the index. An update may consist of multiple messages. * Updates are sent under the identity of the system user of the given organization. * <p> * {@link #IDENTITY_MSG} is the identity element of messages, i.e. identity message will be filtered out */ public void update(final Organization org, final Iterable<P1<? extends Serializable>> messages) { if (updatesCurrent < updatesTotal) { final User user = SecurityUtil.createSystemUser(getSystemUserName(), org); SecurityUtil.runAs(getSecurityService(), org, user, new Effect0() { @Override protected void run() { for (final P1<? extends Serializable> m : $(messages).filter(Booleans.<P1<? extends Serializable>>ne(IDENTITY_MSG))) { getMessageSender().sendObjectMessage(destinationId, MessageSender.DestinationType.Queue, m.get1()); } updatesCurrent = updatesCurrent + 1; getMessageSender().sendObjectMessage( IndexProducer.RESPONSE_QUEUE, MessageSender.DestinationType.Queue, IndexRecreateObject.update( indexName, getService(), updatesTotal, updatesCurrent)); if (updatesCurrent >= updatesTotal) { // send end-of-batch message final Organization emo = endMessageOrg.getOr(org); final User emu = SecurityUtil.createSystemUser(getSystemUserName(), emo); SecurityUtil.runAs(getSecurityService(), emo, emu, new Effect0() { @Override protected void run() { getMessageSender().sendObjectMessage( destinationId, MessageSender.DestinationType.Queue, IndexRecreateObject.end(indexName, getService())); } }); } } }); } else { throw new IllegalStateException(format("The number of allowed update messages (%d) has already been sent", updatesTotal)); } } /** * @see #update(Organization, Iterable) */ public void update(final Organization org, final P1<? extends Serializable>... messages) { update(org, $(messages)); } @Override protected void finalize() throws Throwable { super.finalize(); if (updatesCurrent < updatesTotal) { logger.warn(format("Only %d messages have been sent even though the batch has been initialized with %d", updatesCurrent, updatesTotal)); } } } /* ------------------------------------------------------------------------------------------------------------------ */ private class MessageWatcher implements Runnable { private final Logger logger = LoggerFactory.getLogger(MessageWatcher.class); private volatile boolean listening = true; private volatile FutureTask<Serializable> future; private final ExecutorService executor = Executors.newSingleThreadExecutor(); public void stopListening() { this.listening = false; if (future != null) { future.cancel(true); } executor.shutdown(); } @Override public void run() { if (getMessageReceiver() == null) { logger.warn("The message receiver for " + getClassName() + " was null so unable to listen for repopulate index messages. Ignore this warning if this is a test."); listening = false; return; } logger.info("Starting to listen for {} Messages", getClassName()); while (listening) { try { future = getMessageReceiver().receiveSerializable(IndexProducer.RECEIVER_QUEUE + "." + getService(), MessageSender.DestinationType.Queue); executor.execute(future); BaseMessage message = (BaseMessage) future.get(); if (message == null || !(message.getObject() instanceof IndexRecreateObject)) continue; IndexRecreateObject indexObject = (IndexRecreateObject) message.getObject(); if (!indexObject.getService().equals(getService()) || !indexObject.getStatus().equals(IndexRecreateObject.Status.Start)) continue; logger.info("Index '{}' has received a start repopulating command for service '{}'.", indexObject.getIndexName(), getService()); repopulate(indexObject.getIndexName()); logger.info("Index '{}' has finished repopulating service '{}'.", indexObject.getIndexName(), getService()); } catch (InterruptedException e) { logger.error("Problem while getting {} message events {}", getClassName(), ExceptionUtils.getStackTrace(e)); } catch (ExecutionException e) { logger.error("Problem while getting {} message events {}", getClassName(), ExceptionUtils.getStackTrace(e)); } catch (CancellationException e) { logger.trace("Listening for messages {} has been cancelled.", getClassName()); } catch (Throwable t) { logger.error("Problem while getting {} message events {}", getClassName(), ExceptionUtils.getStackTrace(t)); } } logger.info("Stopping listening for {} Messages", getClassName()); } } }