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(); } } }