/*
* Copyright (c) MuleSoft, Inc. All rights reserved. http://www.mulesoft.com
* The software in this package is published under the terms of the CPAL v1.0
* license, a copy of which has been included with this distribution in the
* LICENSE.txt file.
*/
package org.mule.runtime.core.internal.streaming;
import static org.mule.runtime.api.i18n.I18nMessageFactory.createStaticMessage;
import static reactor.core.publisher.Mono.from;
import org.mule.runtime.api.exception.MuleRuntimeException;
import org.mule.runtime.api.streaming.Cursor;
import org.mule.runtime.api.streaming.CursorProvider;
import org.mule.runtime.api.streaming.bytes.CursorStreamProvider;
import org.mule.runtime.api.streaming.object.CursorIteratorProvider;
import org.mule.runtime.core.api.Event;
import org.mule.runtime.core.api.EventContext;
import org.mule.runtime.core.internal.streaming.bytes.ManagedCursorStreamProvider;
import org.mule.runtime.core.internal.streaming.object.ManagedCursorIteratorProvider;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.cache.RemovalNotification;
import java.util.LinkedList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Keeps track of active {@link Cursor cursors} and their {@link CursorProvider providers}
*
* @since 4.0
*/
public class CursorManager {
private static Logger LOGGER = LoggerFactory.getLogger(CursorManager.class);
private final LoadingCache<String, EventStreamingState> registry =
CacheBuilder.newBuilder()
.removalListener((RemovalNotification<String, EventStreamingState> notification) -> notification.getValue().dispose())
.build(new CacheLoader<String, EventStreamingState>() {
@Override
public EventStreamingState load(String key) throws Exception {
return new EventStreamingState();
}
});
private MutableStreamingStatistics statistics;
/**
* Creates a new instance
*
* @param statistics statistics which values should be kept updated
*/
public CursorManager(MutableStreamingStatistics statistics) {
this.statistics = statistics;
}
/**
* Becomes aware of the given {@code provider} and returns a replacement provider
* which is managed by the runtime, allowing for automatic resource handling
*
* @param provider the provider to be tracked
* @param creatorEvent the event that created the provider
* @return a {@link CursorContext}
*/
public CursorProvider manage(CursorProvider provider, Event creatorEvent) {
final EventContext ownerContext = getRoot(creatorEvent.getContext());
registerEventContext(ownerContext);
registry.getUnchecked(ownerContext.getId()).addProvider(provider);
final CursorContext context = new CursorContext(provider, ownerContext);
if (provider instanceof CursorStreamProvider) {
return new ManagedCursorStreamProvider(context, this);
} else if (provider instanceof CursorIteratorProvider) {
return new ManagedCursorIteratorProvider(context, this);
}
throw new MuleRuntimeException(createStaticMessage("Unknown cursor provider type: " + context.getClass().getName()));
}
/**
* Acknowledges that the given {@code cursor} has been opened
*
* @param cursor the opnened cursor
* @param providerHandle the handle for the provider that generated it
*/
public void onOpen(Cursor cursor, CursorContext providerHandle) {
registry.getUnchecked(providerHandle.getOwnerContext().getId()).addCursor(providerHandle.getCursorProvider(), cursor);
statistics.incrementOpenCursors();
}
/**
* Acknowledges that the given {@code cursor} has been closed
*
* @param cursor the closed cursor
* @param handle the handle for the provider that generated it
*/
public void onClose(Cursor cursor, CursorContext handle) {
final String eventId = handle.getOwnerContext().getId();
EventStreamingState state = registry.getIfPresent(eventId);
if (state != null && state.removeCursor(handle.getCursorProvider(), cursor)) {
state.dispose();
registry.invalidate(eventId);
}
}
private void terminated(EventContext rootContext) {
EventStreamingState state = registry.getIfPresent(rootContext.getId());
if (state != null) {
state.dispose();
registry.invalidate(rootContext.getId());
}
}
/**
* Duplicate registration will occur if cursors are opened in multiple child flows or processing branches. This means terminate
* will fire multiple times sequentially during completion of the parent EventContext. After the first terminate all other
* invocation will literally be no-ops. This is preferred to introducing contention here given multiple thread may be opening
* cursors concurrently.
*/
private void registerEventContext(EventContext eventContext) {
from(eventContext.getCompletionPublisher()).doFinally(signal -> terminated(eventContext)).subscribe();
}
private EventContext getRoot(EventContext eventContext) {
return eventContext.getParentContext()
.map(this::getRoot)
.orElse(eventContext);
}
private class EventStreamingState {
private boolean disposed = false;
private final LoadingCache<CursorProvider, List<Cursor>> cursors = CacheBuilder.newBuilder()
.build(new CacheLoader<CursorProvider, List<Cursor>>() {
@Override
public List<Cursor> load(CursorProvider key) throws Exception {
statistics.incrementOpenProviders();
return new LinkedList<>();
}
});
private synchronized void addProvider(CursorProvider adapter) {
cursors.getUnchecked(adapter);
}
private void addCursor(CursorProvider provider, Cursor cursor) {
cursors.getUnchecked(provider).add(cursor);
}
private boolean removeCursor(CursorProvider provider, Cursor cursor) {
List<Cursor> openCursors = cursors.getUnchecked(provider);
if (openCursors.remove(cursor)) {
statistics.decrementOpenCursors();
}
if (openCursors.isEmpty()) {
if (provider.isClosed()) {
dispose();
cursors.invalidate(provider);
return true;
}
}
return false;
}
private void dispose() {
if (disposed) {
return;
}
cursors.asMap().forEach((provider, cursors) -> {
try {
closeProvider(provider);
releaseAll(cursors);
} finally {
provider.releaseResources();
}
});
disposed = true;
}
private void releaseAll(List<Cursor> cursors) {
cursors.forEach(cursor -> {
try {
cursor.release();
statistics.decrementOpenCursors();
} catch (Exception e) {
LOGGER.warn("Exception was found trying to close cursor. Execution will continue", e);
}
});
}
private void closeProvider(CursorProvider provider) {
if (!provider.isClosed()) {
provider.close();
statistics.decrementOpenProviders();
}
}
}
}