package io.cattle.platform.api.pubsub.subscribe;
import io.cattle.platform.archaius.util.ArchaiusUtil;
import io.cattle.platform.async.retry.CancelRetryException;
import io.cattle.platform.async.retry.Retry;
import io.cattle.platform.async.retry.RetryTimeoutService;
import io.cattle.platform.eventing.EventListener;
import io.cattle.platform.eventing.EventService;
import io.cattle.platform.eventing.model.Event;
import io.cattle.platform.eventing.model.EventVO;
import io.cattle.platform.framework.event.FrameworkEvents;
import io.cattle.platform.framework.event.Ping;
import io.cattle.platform.json.JsonMapper;
import io.github.ibuildthecloud.gdapi.context.ApiContext;
import io.github.ibuildthecloud.gdapi.id.IdFormatter;
import io.github.ibuildthecloud.gdapi.request.ApiRequest;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.inject.Inject;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.util.concurrent.SettableFuture;
import com.netflix.config.DynamicIntProperty;
import com.netflix.config.DynamicLongProperty;
public abstract class NonBlockingSubscriptionHandler implements SubscriptionHandler {
private static final Logger log = LoggerFactory.getLogger(NonBlockingSubscriptionHandler.class);
public static final DynamicLongProperty API_SUB_PING_INVERVAL = ArchaiusUtil.getLong("api.sub.ping.interval.millis");
public static final DynamicIntProperty API_MAX_PINGS = ArchaiusUtil.getInt("api.sub.max.pings");
@Inject
JsonMapper jsonMapper;
@Inject
EventService eventService;
@Inject
RetryTimeoutService retryTimeout;
ExecutorService executorService;
List<ApiPubSubEventPostProcessor> eventProcessors;
@Override
public boolean subscribe(Collection<String> eventNames, final ApiRequest apiRequest, final boolean strip) throws IOException {
ApiContext apiContext = ApiContext.getContext();
final Object writeLock = new Object();
final MessageWriter writer = getMessageWriter(apiRequest);
final AtomicBoolean disconnect = new AtomicBoolean(false);
final IdFormatter idFormatter = apiContext.getIdFormatter();
final Object policy = apiContext.getPolicy();
if (writer == null) {
return false;
}
EventListener listener = new EventListener() {
@Override
public void onEvent(Event event) {
try {
EventVO<Object> modified = new EventVO<Object>(event);
ApiRequest request = new ApiRequest(apiRequest);
if (!postProcess(modified, idFormatter, request, policy)) {
return;
}
obfuscateIds(modified, idFormatter);
write(modified, writer, writeLock, strip);
} catch (IOException e) {
log.trace("IOException on write to client for pub sub, disconnecting", e);
disconnect.set(true);
}
}
};
return subscribe(eventNames, listener, writer, disconnect, writeLock, strip) != null;
}
protected boolean postProcess(EventVO<Object> event, IdFormatter idFormatter, ApiRequest request, Object policy) {
try {
ApiContext context = ApiContext.newContext();
context.setApiRequest(request);
context.setIdFormatter(idFormatter);
context.setPolicy(policy);
for (ApiPubSubEventPostProcessor processor : eventProcessors) {
if (!processor.processEvent(event)) {
return false;
}
}
} finally {
ApiContext.remove();
}
return true;
}
protected void obfuscateIds(EventVO<?> event, IdFormatter idFormatter) {
if (event.getResourceType() == null) {
event.setResourceId(null);
} else {
Object id = idFormatter.formatId(event.getResourceType(), event.getResourceId());
event.setResourceId(id == null ? null : id.toString());
}
}
protected abstract MessageWriter getMessageWriter(ApiRequest apiRequest) throws IOException;
protected void write(Event event, MessageWriter writer, Object writeLock, boolean strip) throws IOException {
EventVO<Object> newEvent = new EventVO<Object>(event);
if (strip) {
String name = newEvent.getName();
if (name != null) {
newEvent.setName(StringUtils.substringBefore(name, FrameworkEvents.EVENT_SEP));
}
}
String content = jsonMapper.writeValueAsString(newEvent);
write(writer, content, writeLock);
}
protected void write(MessageWriter writer, String content, Object writeLock) throws IOException {
writer.write(content, writeLock);
}
protected Future<?> subscribe(Collection<String> eventNames, EventListener listener, MessageWriter writer, AtomicBoolean disconnect, Object writeLock,
boolean strip) {
boolean unsubscribe = false;
try {
for (String eventName : eventNames) {
eventService.subscribe(eventName, listener).get(API_SUB_PING_INVERVAL.get(), TimeUnit.MILLISECONDS);
}
write(new Ping(), writer, writeLock, strip);
return schedulePing(listener, writer, disconnect);
} catch (Throwable e) {
unsubscribe = true;
} finally {
if (unsubscribe) {
unsubscribe(disconnect, writer, listener);
}
}
return null;
}
protected Future<?> schedulePing(final EventListener listener, final MessageWriter writer, final AtomicBoolean disconnect) {
final SettableFuture<?> future = SettableFuture.create();
retryTimeout.submit(new Retry(API_MAX_PINGS.get(), API_SUB_PING_INVERVAL.get(), future, new Runnable() {
@Override
public void run() {
if (disconnect.get()) {
unsubscribe(disconnect, writer, listener);
future.setException(new CancelRetryException());
throw new CancelRetryException();
}
listener.onEvent(new Ping());
}
}));
future.addListener(new Runnable() {
@Override
public void run() {
try {
future.get();
} catch (InterruptedException e) {
unsubscribe(disconnect, writer, listener);
} catch (ExecutionException e) {
unsubscribe(disconnect, writer, listener);
}
}
}, executorService);
return future;
}
protected void unsubscribe(AtomicBoolean disconnect, MessageWriter writer, EventListener listener) {
disconnect.set(true);
writer.close();
eventService.unsubscribe(listener);
}
public List<ApiPubSubEventPostProcessor> getEventProcessors() {
return eventProcessors;
}
@Inject
public void setEventProcessors(List<ApiPubSubEventPostProcessor> eventProcessors) {
this.eventProcessors = eventProcessors;
}
public ExecutorService getExecutorService() {
return executorService;
}
public void setExecutorService(ExecutorService executorService) {
this.executorService = executorService;
}
}