Added CronScheduler support as a proof to clock drift while emitting metrics (#10448)

Co-authored-by: Ayush Kulshrestha <ayush.kulshrestha@miqdigital.com>
This commit is contained in:
Ayush Kulshrestha 2020-11-25 17:01:38 +05:30 committed by GitHub
parent fe693a4f01
commit d0c2ede50c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 423 additions and 25 deletions

View File

@ -224,6 +224,10 @@
<groupId>org.antlr</groupId> <groupId>org.antlr</groupId>
<artifactId>antlr4-runtime</artifactId> <artifactId>antlr4-runtime</artifactId>
</dependency> </dependency>
<dependency>
<groupId>io.timeandspace</groupId>
<artifactId>cron-scheduler</artifactId>
</dependency>

View File

@ -22,12 +22,17 @@ package org.apache.druid.java.util.metrics;
import org.apache.druid.java.util.emitter.service.ServiceEmitter; import org.apache.druid.java.util.emitter.service.ServiceEmitter;
import java.util.concurrent.Future;
/** /**
*/ */
public abstract class AbstractMonitor implements Monitor public abstract class AbstractMonitor implements Monitor
{ {
private volatile boolean started = false; private volatile boolean started = false;
private volatile Future<?> scheduledFuture;
@Override @Override
public void start() public void start()
{ {
@ -51,4 +56,16 @@ public abstract class AbstractMonitor implements Monitor
} }
public abstract boolean doMonitor(ServiceEmitter emitter); public abstract boolean doMonitor(ServiceEmitter emitter);
@Override
public Future<?> getScheduledFuture()
{
return scheduledFuture;
}
@Override
public void setScheduledFuture(Future<?> scheduledFuture)
{
this.scheduledFuture = scheduledFuture;
}
} }

View File

@ -24,11 +24,14 @@ import org.apache.druid.java.util.emitter.service.ServiceEmitter;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.concurrent.Future;
public abstract class CompoundMonitor implements Monitor public abstract class CompoundMonitor implements Monitor
{ {
private final List<Monitor> monitors; private final List<Monitor> monitors;
private volatile Future<?> scheduledFuture;
public CompoundMonitor(List<Monitor> monitors) public CompoundMonitor(List<Monitor> monitors)
{ {
this.monitors = monitors; this.monitors = monitors;
@ -61,5 +64,17 @@ public abstract class CompoundMonitor implements Monitor
return shouldReschedule(Lists.transform(monitors, monitor -> monitor.monitor(emitter))); return shouldReschedule(Lists.transform(monitors, monitor -> monitor.monitor(emitter)));
} }
@Override
public Future<?> getScheduledFuture()
{
return scheduledFuture;
}
@Override
public void setScheduledFuture(Future<?> scheduledFuture)
{
this.scheduledFuture = scheduledFuture;
}
public abstract boolean shouldReschedule(List<Boolean> reschedules); public abstract boolean shouldReschedule(List<Boolean> reschedules);
} }

View File

@ -21,6 +21,9 @@ package org.apache.druid.java.util.metrics;
import org.apache.druid.java.util.emitter.service.ServiceEmitter; import org.apache.druid.java.util.emitter.service.ServiceEmitter;
import java.util.concurrent.Future;
/** /**
*/ */
public interface Monitor public interface Monitor
@ -35,4 +38,8 @@ public interface Monitor
* @return true if this monitor needs to continue monitoring. False otherwise. * @return true if this monitor needs to continue monitoring. False otherwise.
*/ */
boolean monitor(ServiceEmitter emitter); boolean monitor(ServiceEmitter emitter);
Future<?> getScheduledFuture();
void setScheduledFuture(Future<?> scheduledFuture);
} }

View File

@ -20,41 +20,52 @@
package org.apache.druid.java.util.metrics; package org.apache.druid.java.util.metrics;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import io.timeandspace.cronscheduler.CronScheduler;
import io.timeandspace.cronscheduler.CronTask;
import org.apache.druid.java.util.common.ISE; import org.apache.druid.java.util.common.ISE;
import org.apache.druid.java.util.common.concurrent.ScheduledExecutors;
import org.apache.druid.java.util.common.lifecycle.LifecycleStart; import org.apache.druid.java.util.common.lifecycle.LifecycleStart;
import org.apache.druid.java.util.common.lifecycle.LifecycleStop; import org.apache.druid.java.util.common.lifecycle.LifecycleStop;
import org.apache.druid.java.util.common.logger.Logger;
import org.apache.druid.java.util.emitter.service.ServiceEmitter; import org.apache.druid.java.util.emitter.service.ServiceEmitter;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
/** /**
*/ */
public class MonitorScheduler public class MonitorScheduler
{ {
private static final Logger log = new Logger(MonitorScheduler.class);
private final MonitorSchedulerConfig config; private final MonitorSchedulerConfig config;
private final ScheduledExecutorService exec;
private final ServiceEmitter emitter; private final ServiceEmitter emitter;
private final Set<Monitor> monitors; private final Set<Monitor> monitors;
private final Object lock = new Object(); private final Object lock = new Object();
private final CronScheduler scheduler;
private final ExecutorService executorService;
private volatile boolean started = false; private volatile boolean started = false;
public MonitorScheduler( public MonitorScheduler(
MonitorSchedulerConfig config, MonitorSchedulerConfig config,
ScheduledExecutorService exec, CronScheduler scheduler,
ServiceEmitter emitter, ServiceEmitter emitter,
List<Monitor> monitors List<Monitor> monitors,
ExecutorService executorService
) )
{ {
this.config = config; this.config = config;
this.exec = exec; this.scheduler = scheduler;
this.emitter = emitter; this.emitter = emitter;
this.monitors = Sets.newHashSet(monitors); this.monitors = Sets.newHashSet(monitors);
this.executorService = executorService;
} }
@LifecycleStart @LifecycleStart
@ -124,24 +135,47 @@ public class MonitorScheduler
{ {
synchronized (lock) { synchronized (lock) {
monitor.start(); monitor.start();
ScheduledExecutors.scheduleAtFixedRate( long rate = config.getEmitterPeriod().getMillis();
exec, Future<?> scheduledFuture = scheduler.scheduleAtFixedRate(
config.getEmitterPeriod(), rate,
new Callable<ScheduledExecutors.Signal>() rate,
TimeUnit.MILLISECONDS,
new CronTask()
{
private volatile Future<Boolean> monitorFuture = null;
@Override
public void run(long scheduledRunTimeMillis)
{
try {
if (monitorFuture != null && monitorFuture.isDone()
&& !(monitorFuture.get() && hasMonitor(monitor))) {
removeMonitor(monitor);
monitor.getScheduledFuture().cancel(false);
log.debug("Stopped rescheduling %s (delay %s)", this, rate);
return;
}
log.trace("Running %s (period %s)", this, rate);
monitorFuture = executorService.submit(new Callable<Boolean>()
{ {
@Override @Override
public ScheduledExecutors.Signal call() public Boolean call()
{ {
// Run one more time even if the monitor was removed, in case there's some extra data to flush try {
if (monitor.monitor(emitter) && hasMonitor(monitor)) { return monitor.monitor(emitter);
return ScheduledExecutors.Signal.REPEAT; }
} else { catch (Throwable e) {
removeMonitor(monitor); log.error(e, "Uncaught exception.");
return ScheduledExecutors.Signal.STOP; return Boolean.FALSE;
} }
} }
});
} }
); catch (Throwable e) {
log.error(e, "Uncaught exception.");
}
}
});
monitor.setScheduledFuture(scheduledFuture);
} }
} }
@ -151,4 +185,5 @@ public class MonitorScheduler
return monitors.contains(monitor); return monitors.contains(monitor);
} }
} }
} }

View File

@ -19,17 +19,41 @@
package org.apache.druid.java.util.metrics; package org.apache.druid.java.util.metrics;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import org.apache.druid.java.util.common.concurrent.Execs; import io.timeandspace.cronscheduler.CronScheduler;
import io.timeandspace.cronscheduler.CronTask;
import org.apache.druid.java.util.emitter.service.ServiceEmitter; import org.apache.druid.java.util.emitter.service.ServiceEmitter;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.time.Duration;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
public class MonitorSchedulerTest public class MonitorSchedulerTest
{ {
@Mock
private CronScheduler cronScheduler;
@Before
public void setUp()
{
MockitoAnnotations.initMocks(this);
}
@Test @Test
public void testFindMonitor() public void testFindMonitor()
{ {
@ -46,11 +70,14 @@ public class MonitorSchedulerTest
final Monitor1 monitor1 = new Monitor1(); final Monitor1 monitor1 = new Monitor1();
final Monitor2 monitor2 = new Monitor2(); final Monitor2 monitor2 = new Monitor2();
ExecutorService executor = Mockito.mock(ExecutorService.class);
final MonitorScheduler scheduler = new MonitorScheduler( final MonitorScheduler scheduler = new MonitorScheduler(
Mockito.mock(MonitorSchedulerConfig.class), Mockito.mock(MonitorSchedulerConfig.class),
Execs.scheduledSingleThreaded("monitor-scheduler-test"), CronScheduler.newBuilder(Duration.ofSeconds(1L)).setThreadName("monitor-scheduler-test").build(),
Mockito.mock(ServiceEmitter.class), Mockito.mock(ServiceEmitter.class),
ImmutableList.of(monitor1, monitor2) ImmutableList.of(monitor1, monitor2),
executor
); );
final Optional<Monitor1> maybeFound1 = scheduler.findMonitor(Monitor1.class); final Optional<Monitor1> maybeFound1 = scheduler.findMonitor(Monitor1.class);
@ -63,6 +90,263 @@ public class MonitorSchedulerTest
Assert.assertFalse(scheduler.findMonitor(Monitor3.class).isPresent()); Assert.assertFalse(scheduler.findMonitor(Monitor3.class).isPresent());
} }
@Test
public void testStart_RepeatScheduling()
{
ExecutorService executor = Mockito.mock(ExecutorService.class);
Mockito.doAnswer(new Answer<Future<?>>()
{
private int scheduleCount = 0;
@SuppressWarnings("unchecked")
@Override
public Future<?> answer(InvocationOnMock invocation) throws Exception
{
final Object originalArgument = (invocation.getArguments())[3];
CronTask task = ((CronTask) originalArgument);
Mockito.doAnswer(new Answer<Future<?>>()
{
@Override
public Future<Boolean> answer(InvocationOnMock invocation) throws Exception
{
final Object originalArgument = (invocation.getArguments())[0];
((Callable<Boolean>) originalArgument).call();
return CompletableFuture.completedFuture(Boolean.TRUE);
}
}).when(executor).submit(ArgumentMatchers.any(Callable.class));
while (scheduleCount < 2) {
scheduleCount++;
task.run(123L);
}
return createDummyFuture();
}
}).when(cronScheduler).scheduleAtFixedRate(ArgumentMatchers.anyLong(), ArgumentMatchers.anyLong(),
ArgumentMatchers.any(), ArgumentMatchers.any(CronTask.class));
Monitor monitor = Mockito.mock(Monitor.class);
MonitorSchedulerConfig config = Mockito.mock(MonitorSchedulerConfig.class);
Mockito.when(config.getEmitterPeriod()).thenReturn(new org.joda.time.Duration(1000L));
final MonitorScheduler scheduler = new MonitorScheduler(
config,
cronScheduler,
Mockito.mock(ServiceEmitter.class),
ImmutableList.of(monitor),
executor);
scheduler.start();
Mockito.verify(monitor, Mockito.times(1)).start();
Mockito.verify(cronScheduler, Mockito.times(1)).scheduleAtFixedRate(ArgumentMatchers.anyLong(),
ArgumentMatchers.anyLong(),
ArgumentMatchers.any(), ArgumentMatchers.any(CronTask.class));
Mockito.verify(executor, Mockito.times(2)).submit(ArgumentMatchers.any(Callable.class));
Mockito.verify(monitor, Mockito.times(2)).monitor(ArgumentMatchers.any());
}
@Test
public void testStart_RepeatAndStopScheduling()
{
ExecutorService executor = Mockito.mock(ExecutorService.class);
Mockito.doAnswer(new Answer<Future<?>>()
{
private int scheduleCount = 0;
@SuppressWarnings("unchecked")
@Override
public Future<?> answer(InvocationOnMock invocation) throws Exception
{
final Object originalArgument = (invocation.getArguments())[3];
CronTask task = ((CronTask) originalArgument);
Mockito.doAnswer(new Answer<Future<?>>()
{
@Override
public Future<Boolean> answer(InvocationOnMock invocation) throws Exception
{
final Object originalArgument = (invocation.getArguments())[0];
((Callable<Boolean>) originalArgument).call();
return CompletableFuture.completedFuture(Boolean.FALSE);
}
}).when(executor).submit(ArgumentMatchers.any(Callable.class));
while (scheduleCount < 2) {
scheduleCount++;
task.run(123L);
}
return createDummyFuture();
}
}).when(cronScheduler).scheduleAtFixedRate(ArgumentMatchers.anyLong(), ArgumentMatchers.anyLong(),
ArgumentMatchers.any(), ArgumentMatchers.any(CronTask.class));
Monitor monitor = Mockito.mock(Monitor.class);
Mockito.when(monitor.getScheduledFuture()).thenReturn(createDummyFuture());
MonitorSchedulerConfig config = Mockito.mock(MonitorSchedulerConfig.class);
Mockito.when(config.getEmitterPeriod()).thenReturn(new org.joda.time.Duration(1000L));
final MonitorScheduler scheduler = new MonitorScheduler(
config,
cronScheduler,
Mockito.mock(ServiceEmitter.class),
ImmutableList.of(monitor),
executor);
scheduler.start();
Mockito.verify(monitor, Mockito.times(1)).start();
Mockito.verify(cronScheduler, Mockito.times(1)).scheduleAtFixedRate(ArgumentMatchers.anyLong(),
ArgumentMatchers.anyLong(),
ArgumentMatchers.any(), ArgumentMatchers.any(CronTask.class));
Mockito.verify(executor, Mockito.times(1)).submit(ArgumentMatchers.any(Callable.class));
Mockito.verify(monitor, Mockito.times(1)).monitor(ArgumentMatchers.any());
Mockito.verify(monitor, Mockito.times(1)).stop();
}
@Test
public void testStart_UnexpectedExceptionWhileMonitoring()
{
ExecutorService executor = Mockito.mock(ExecutorService.class);
Monitor monitor = Mockito.mock(Monitor.class);
Mockito.when(monitor.getScheduledFuture()).thenReturn(createDummyFuture());
Mockito.when(monitor.monitor(ArgumentMatchers.any(ServiceEmitter.class))).thenThrow(new RuntimeException());
MonitorSchedulerConfig config = Mockito.mock(MonitorSchedulerConfig.class);
Mockito.when(config.getEmitterPeriod()).thenReturn(new org.joda.time.Duration(1000L));
Mockito.doAnswer(new Answer<Future<?>>()
{
@SuppressWarnings("unchecked")
@Override
public Future<?> answer(InvocationOnMock invocation) throws Exception
{
final Object originalArgument = (invocation.getArguments())[3];
CronTask task = ((CronTask) originalArgument);
Mockito.doAnswer(new Answer<Future<?>>()
{
@Override
public Future<Boolean> answer(InvocationOnMock invocation) throws Exception
{
final Object originalArgument = (invocation.getArguments())[0];
((Callable<Boolean>) originalArgument).call();
return CompletableFuture.completedFuture(Boolean.TRUE);
}
}).when(executor).submit(ArgumentMatchers.any(Callable.class));
task.run(123L);
return createDummyFuture();
}
}).when(cronScheduler).scheduleAtFixedRate(ArgumentMatchers.anyLong(), ArgumentMatchers.anyLong(),
ArgumentMatchers.any(), ArgumentMatchers.any(CronTask.class));
final MonitorScheduler scheduler = new MonitorScheduler(
config,
cronScheduler,
Mockito.mock(ServiceEmitter.class),
ImmutableList.of(monitor),
executor);
scheduler.start();
Mockito.verify(monitor, Mockito.times(1)).start();
Mockito.verify(cronScheduler, Mockito.times(1)).scheduleAtFixedRate(ArgumentMatchers.anyLong(),
ArgumentMatchers.anyLong(),
ArgumentMatchers.any(), ArgumentMatchers.any(CronTask.class));
Mockito.verify(executor, Mockito.times(1)).submit(ArgumentMatchers.any(Callable.class));
Mockito.verify(monitor, Mockito.times(1)).monitor(ArgumentMatchers.any());
}
@Test
public void testStart_UnexpectedExceptionWhileScheduling()
{
ExecutorService executor = Mockito.mock(ExecutorService.class);
Monitor monitor = Mockito.mock(Monitor.class);
Mockito.when(monitor.getScheduledFuture()).thenReturn(createDummyFuture());
MonitorSchedulerConfig config = Mockito.mock(MonitorSchedulerConfig.class);
Mockito.when(config.getEmitterPeriod()).thenReturn(new org.joda.time.Duration(1000L));
Mockito.doAnswer(new Answer<Future<?>>()
{
@SuppressWarnings("unchecked")
@Override
public Future<?> answer(InvocationOnMock invocation) throws Exception
{
final Object originalArgument = (invocation.getArguments())[3];
CronTask task = ((CronTask) originalArgument);
Mockito.when(executor.submit(ArgumentMatchers.any(Callable.class))).thenThrow(new RuntimeException());
task.run(123L);
return createDummyFuture();
}
}).when(cronScheduler).scheduleAtFixedRate(ArgumentMatchers.anyLong(), ArgumentMatchers.anyLong(),
ArgumentMatchers.any(), ArgumentMatchers.any(CronTask.class));
final MonitorScheduler scheduler = new MonitorScheduler(
config,
cronScheduler,
Mockito.mock(ServiceEmitter.class),
ImmutableList.of(monitor),
executor);
scheduler.start();
Mockito.verify(monitor, Mockito.times(1)).start();
Mockito.verify(cronScheduler, Mockito.times(1)).scheduleAtFixedRate(ArgumentMatchers.anyLong(),
ArgumentMatchers.anyLong(),
ArgumentMatchers.any(), ArgumentMatchers.any(CronTask.class));
Mockito.verify(executor, Mockito.times(1)).submit(ArgumentMatchers.any(Callable.class));
}
private Future createDummyFuture()
{
Future<?> future = new Future()
{
@Override
public boolean cancel(boolean mayInterruptIfRunning)
{
return false;
}
@Override
public boolean isCancelled()
{
return false;
}
@Override
public boolean isDone()
{
return false;
}
@Override
public Object get()
{
return null;
}
@Override
public Object get(long timeout, TimeUnit unit)
{
return null;
}
};
return future;
}
private static class NoopMonitor implements Monitor private static class NoopMonitor implements Monitor
{ {
@Override @Override
@ -82,5 +366,19 @@ public class MonitorSchedulerTest
{ {
return true; return true;
} }
@Override
public Future<?> getScheduledFuture()
{
// TODO Auto-generated method stub
return null;
}
@Override
public void setScheduledFuture(Future<?> scheduledFuture)
{
// TODO Auto-generated method stub
}
} }
} }

View File

@ -345,6 +345,16 @@ libraries:
--- ---
name: CronScheduler
license_category: binary
module: java-core
license_name: Apache License version 2.0
version: 0.1
libraries:
- io.timeandspace: cron-scheduler
---
name: LMAX Disruptor name: LMAX Disruptor
license_category: binary license_category: binary
module: java-core module: java-core

View File

@ -1255,6 +1255,11 @@
<version>1.19.0</version> <version>1.19.0</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>io.timeandspace</groupId>
<artifactId>cron-scheduler</artifactId>
<version>0.1</version>
</dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>

View File

@ -311,6 +311,10 @@
<groupId>io.github.resilience4j</groupId> <groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-bulkhead</artifactId> <artifactId>resilience4j-bulkhead</artifactId>
</dependency> </dependency>
<dependency>
<groupId>io.timeandspace</groupId>
<artifactId>cron-scheduler</artifactId>
</dependency>
<!-- Tests --> <!-- Tests -->
<dependency> <dependency>

View File

@ -27,6 +27,7 @@ import com.google.inject.Key;
import com.google.inject.Module; import com.google.inject.Module;
import com.google.inject.Provides; import com.google.inject.Provides;
import com.google.inject.name.Names; import com.google.inject.name.Names;
import io.timeandspace.cronscheduler.CronScheduler;
import org.apache.druid.guice.DruidBinders; import org.apache.druid.guice.DruidBinders;
import org.apache.druid.guice.JsonConfigProvider; import org.apache.druid.guice.JsonConfigProvider;
import org.apache.druid.guice.LazySingleton; import org.apache.druid.guice.LazySingleton;
@ -42,6 +43,7 @@ import org.apache.druid.java.util.metrics.MonitorScheduler;
import org.apache.druid.java.util.metrics.SysMonitor; import org.apache.druid.java.util.metrics.SysMonitor;
import org.apache.druid.query.ExecutorServiceMonitor; import org.apache.druid.query.ExecutorServiceMonitor;
import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -106,9 +108,10 @@ public class MetricsModule implements Module
return new MonitorScheduler( return new MonitorScheduler(
config.get(), config.get(),
Execs.scheduledSingleThreaded("MonitorScheduler-%s"), CronScheduler.newBuilder(Duration.ofSeconds(1L)).setThreadName("MonitorSchedulerThread").build(),
emitter, emitter,
monitors monitors,
Execs.multiThreaded(64, "MonitorThread-%d")
); );
} }