package com.airbnb.airpal.resources.sse;
import com.airbnb.airpal.api.Job;
import com.airbnb.airpal.api.event.JobEvent;
import com.airbnb.airpal.api.event.JobFinishedEvent;
import com.airbnb.airpal.api.event.JobUpdateEvent;
import com.airbnb.airpal.core.AirpalUser;
import com.airbnb.airpal.core.AirpalUserFactory;
import com.airbnb.airpal.core.AuthorizationUtil;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Iterables;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import com.google.common.util.concurrent.RateLimiter;
import com.google.inject.Inject;
import com.google.inject.name.Named;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jetty.servlets.EventSource;
import org.eclipse.jetty.servlets.EventSourceServlet;
import javax.servlet.http.HttpServletRequest;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import static com.codahale.metrics.MetricRegistry.name;
import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j
public class SSEEventSourceServlet extends EventSourceServlet
{
private final JobUpdateToSSERelay jobUpdateToSSERelay;
private final AirpalUserFactory userFactory;
@Inject
public SSEEventSourceServlet(ObjectMapper objectMapper,
EventBus eventBus,
@Named("sse") ExecutorService executorService,
MetricRegistry registry,
AirpalUserFactory userFactory)
{
this.jobUpdateToSSERelay = new JobUpdateToSSERelay(objectMapper, executorService, registry);
this.userFactory = userFactory;
eventBus.register(jobUpdateToSSERelay);
}
@Override
protected EventSource newEventSource(HttpServletRequest request)
{
SSEEventSource eventSource = new SSEEventSource(jobUpdateToSSERelay);
jobUpdateToSSERelay.addListener(eventSource, userFactory.provide());
return eventSource;
}
static class JobUpdateToSSERelay
{
private final ObjectMapper objectMapper;
private final RateLimiter updateLimiter = RateLimiter.create(15.0);
private final Set<SSEEventSource> subscribers = Collections.newSetFromMap(new ConcurrentHashMap<SSEEventSource, Boolean>());
private final Map<SSEEventSource, AirpalUser> eventSourceSubjectMap = new ConcurrentHashMap<>();
private final ExecutorService executorService;
private final Timer timer;
public JobUpdateToSSERelay(ObjectMapper objectMapper, ExecutorService executorService, MetricRegistry registry)
{
this.objectMapper = checkNotNull(objectMapper, "objectMapper was null");
this.executorService = checkNotNull(executorService, "executorService was null");
this.timer = registry.timer(name(AuthorizedEventBroadcast.class, "authorization"));
}
public void addListener(SSEEventSource sseEventSource, AirpalUser subject)
{
AirpalUser eventSubject = checkNotNull(subject, "subject was null");
SSEEventSource eventSource = checkNotNull(sseEventSource, "sseEventSource was null");
subscribers.add(eventSource);
eventSourceSubjectMap.put(eventSource, eventSubject);
}
public void removeListener(SSEEventSource sseEventSource)
{
SSEEventSource eventSource = checkNotNull(sseEventSource, "sseEventSource was null");
subscribers.remove(eventSource);
eventSourceSubjectMap.remove(eventSource);
}
private void broadcast(JobEvent message)
{
try {
String jsonMessage = objectMapper.writeValueAsString(message);
for (SSEEventSource subscriber : subscribers) {
executorService.submit(
new AuthorizedEventBroadcast(subscriber,
eventSourceSubjectMap.get(subscriber),
jsonMessage,
message.getJob(),
timer));
}
}
catch (JsonProcessingException e) {
log.error("Could not serialize JobEvent as JSON", e);
}
}
@Subscribe
public void receiveJobUpdate(JobUpdateEvent event) {
if (updateLimiter.tryAcquire()) {
broadcast(event);
}
}
@Subscribe
public void receiveJobFinished(JobFinishedEvent event) {
broadcast(event);
}
}
@Value
private static class AuthorizedEventBroadcast implements Runnable
{
private final SSEEventSource eventSource;
private final AirpalUser subject;
private final String message;
private final Job job;
private final Timer timer;
@Override
public void run()
{
Timer.Context context = timer.time();
if (Iterables.all(job.getTablesUsed(), new AuthorizationUtil.AuthorizedTablesPredicate(subject))) {
eventSource.emit(message);
}
context.stop();
}
}
}