/**
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.aurora.scheduler.reconciliation;
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.concurrent.Executor;
import javax.inject.Singleton;
import com.google.common.eventbus.EventBus;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import org.apache.aurora.common.quantity.Amount;
import org.apache.aurora.common.quantity.Time;
import org.apache.aurora.common.stats.StatsProvider;
import org.apache.aurora.common.testing.easymock.EasyMockTest;
import org.apache.aurora.common.util.BackoffStrategy;
import org.apache.aurora.gen.AssignedTask;
import org.apache.aurora.gen.ScheduleStatus;
import org.apache.aurora.gen.ScheduledTask;
import org.apache.aurora.scheduler.app.LifecycleModule;
import org.apache.aurora.scheduler.async.AsyncModule.AsyncExecutor;
import org.apache.aurora.scheduler.async.DelayExecutor;
import org.apache.aurora.scheduler.base.Query;
import org.apache.aurora.scheduler.events.PubsubEvent.TaskStateChange;
import org.apache.aurora.scheduler.events.PubsubEventModule;
import org.apache.aurora.scheduler.mesos.Driver;
import org.apache.aurora.scheduler.state.PubsubTestUtil;
import org.apache.aurora.scheduler.storage.Storage;
import org.apache.aurora.scheduler.storage.entities.IScheduledTask;
import org.apache.aurora.scheduler.storage.testing.StorageTestUtil;
import org.apache.aurora.scheduler.testing.FakeScheduledExecutor;
import org.apache.aurora.scheduler.testing.FakeStatsProvider;
import org.junit.Before;
import org.junit.Test;
import static org.apache.aurora.gen.ScheduleStatus.KILLING;
import static org.apache.aurora.gen.ScheduleStatus.RUNNING;
import static org.easymock.EasyMock.expect;
import static org.junit.Assert.assertEquals;
public class KillRetryTest extends EasyMockTest {
private Driver driver;
private StorageTestUtil storageUtil;
private BackoffStrategy backoffStrategy;
private FakeScheduledExecutor clock;
private EventBus eventBus;
private FakeStatsProvider statsProvider;
@Before
public void setUp() throws Exception {
driver = createMock(Driver.class);
storageUtil = new StorageTestUtil(this);
storageUtil.expectOperations();
backoffStrategy = createMock(BackoffStrategy.class);
final DelayExecutor executorMock = createMock(DelayExecutor.class);
clock = FakeScheduledExecutor.fromDelayExecutor(executorMock);
addTearDown(clock::assertEmpty);
statsProvider = new FakeStatsProvider();
Injector injector = Guice.createInjector(
new LifecycleModule(),
new PubsubEventModule(),
new AbstractModule() {
@Override
protected void configure() {
bind(Driver.class).toInstance(driver);
bind(Storage.class).toInstance(storageUtil.storage);
bind(DelayExecutor.class).annotatedWith(AsyncExecutor.class).toInstance(executorMock);
PubsubEventModule.bindSubscriber(binder(), KillRetry.class);
bind(KillRetry.class).in(Singleton.class);
bind(BackoffStrategy.class).toInstance(backoffStrategy);
bind(StatsProvider.class).toInstance(statsProvider);
bind(UncaughtExceptionHandler.class)
.toInstance(createMock(UncaughtExceptionHandler.class));
bind(Executor.class).annotatedWith(AsyncExecutor.class)
.toInstance(MoreExecutors.sameThreadExecutor());
}
}
);
eventBus = injector.getInstance(EventBus.class);
PubsubTestUtil.startPubsub(injector);
}
private static IScheduledTask makeTask(String id, ScheduleStatus status) {
return IScheduledTask.build(new ScheduledTask()
.setStatus(status)
.setAssignedTask(new AssignedTask().setTaskId(id)));
}
private void moveToKilling(String taskId) {
eventBus.post(TaskStateChange.transition(makeTask(taskId, KILLING), RUNNING));
}
private static Query.Builder killingQuery(String taskId) {
return Query.taskScoped(taskId).byStatus(KILLING);
}
private void expectGetRetryDelay(long prevRetryMs, long retryInMs) {
expect(backoffStrategy.calculateBackoffMs(prevRetryMs)).andReturn(retryInMs);
}
private void expectRetry(String taskId, long prevRetryMs, long nextRetryMs) {
storageUtil.expectTaskFetch(killingQuery(taskId), makeTask(taskId, KILLING));
driver.killTask(taskId);
expectGetRetryDelay(prevRetryMs, nextRetryMs);
}
@Test
public void testRetries() {
String taskId = "a";
expectGetRetryDelay(0, 100);
expectRetry(taskId, 100, 1000);
expectRetry(taskId, 1000, 10000);
// Signal that task has transitioned.
storageUtil.expectTaskFetch(killingQuery(taskId));
control.replay();
moveToKilling(taskId);
clock.advance(Amount.of(100L, Time.MILLISECONDS));
clock.advance(Amount.of(1000L, Time.MILLISECONDS));
clock.advance(Amount.of(10000L, Time.MILLISECONDS));
assertEquals(2L, statsProvider.getLongValue(KillRetry.RETRIES_COUNTER));
}
@Test
public void testDoesNotRetry() {
String taskId = "a";
expectGetRetryDelay(0, 100);
storageUtil.expectTaskFetch(killingQuery(taskId));
control.replay();
moveToKilling(taskId);
clock.advance(Amount.of(100L, Time.MILLISECONDS));
assertEquals(0L, statsProvider.getLongValue(KillRetry.RETRIES_COUNTER));
}
}