/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 gobblin.runtime; import java.io.IOException; import java.util.Map; import java.util.Properties; import java.util.Queue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.testng.Assert; import org.testng.annotations.Test; import com.google.common.base.Predicate; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Queues; import gobblin.commit.DeliverySemantics; import gobblin.configuration.ConfigurationKeys; import gobblin.util.Either; import javax.annotation.Nullable; import lombok.extern.slf4j.Slf4j; @Slf4j public class JobContextTest { @Test public void testNonParallelCommit() throws Exception { Properties jobProps = new Properties(); jobProps.setProperty(ConfigurationKeys.JOB_NAME_KEY, "test"); jobProps.setProperty(ConfigurationKeys.JOB_ID_KEY, "job_id_12345"); jobProps.setProperty(ConfigurationKeys.METRICS_ENABLED_KEY, "false"); Map<String, JobState.DatasetState> datasetStateMap = Maps.newHashMap(); for (int i = 0; i < 2; i++) { datasetStateMap.put(Integer.toString(i), new JobState.DatasetState()); } final BlockingQueue<ControllableCallable<Void>> callables = Queues.newLinkedBlockingQueue(); final JobContext jobContext = new ControllableCommitJobContext(jobProps, log, datasetStateMap, new Predicate<String>() { @Override public boolean apply(@Nullable String input) { return true; } }, callables); ExecutorService executorService = Executors.newSingleThreadExecutor(); Future future = executorService.submit(new Runnable() { @Override public void run() { try { jobContext.commit(); } catch (IOException ioe) { throw new RuntimeException(ioe); } } }); // Not parallelized, should only one commit running ControllableCallable<Void> callable = callables.poll(1, TimeUnit.SECONDS); Assert.assertNotNull(callable); Assert.assertNull(callables.poll(200, TimeUnit.MILLISECONDS)); // unblock first commit, should see a second commit callable.unblock(); callable = callables.poll(1, TimeUnit.SECONDS); Assert.assertNotNull(callable); Assert.assertNull(callables.poll(200, TimeUnit.MILLISECONDS)); Assert.assertFalse(future.isDone()); // unblock second commit, commit should complete callable.unblock(); future.get(1, TimeUnit.SECONDS); Assert.assertEquals(jobContext.getJobState().getState(), JobState.RunningState.COMMITTED); } @Test public void testParallelCommit() throws Exception { Properties jobProps = new Properties(); jobProps.setProperty(ConfigurationKeys.JOB_NAME_KEY, "test"); jobProps.setProperty(ConfigurationKeys.JOB_ID_KEY, "job_id_12345"); jobProps.setProperty(ConfigurationKeys.METRICS_ENABLED_KEY, "false"); jobProps.setProperty(ConfigurationKeys.PARALLELIZE_DATASET_COMMIT, "true"); Map<String, JobState.DatasetState> datasetStateMap = Maps.newHashMap(); for (int i = 0; i < 5; i++) { datasetStateMap.put(Integer.toString(i), new JobState.DatasetState()); } final BlockingQueue<ControllableCallable<Void>> callables = Queues.newLinkedBlockingQueue(); final JobContext jobContext = new ControllableCommitJobContext(jobProps, log, datasetStateMap, new Predicate<String>() { @Override public boolean apply(@Nullable String input) { return true; } }, callables); ExecutorService executorService = Executors.newSingleThreadExecutor(); Future future = executorService.submit(new Runnable() { @Override public void run() { try { jobContext.commit(); } catch (IOException ioe) { throw new RuntimeException(ioe); } } }); // Parallelized, should be able to get all 5 commits running Queue<ControllableCallable<Void>> drainedCallables = Lists.newLinkedList(); Assert.assertEquals(Queues.drain(callables, drainedCallables, 5, 1, TimeUnit.SECONDS), 5); Assert.assertFalse(future.isDone()); // unblock all commits for (ControllableCallable<Void> callable : drainedCallables) { callable.unblock(); } // check that future is done future.get(1, TimeUnit.SECONDS); // check that no more commits were added Assert.assertTrue(callables.isEmpty()); Assert.assertEquals(jobContext.getJobState().getState(), JobState.RunningState.COMMITTED); } @Test public void testSingleExceptionSemantics() throws Exception { Properties jobProps = new Properties(); jobProps.setProperty(ConfigurationKeys.JOB_NAME_KEY, "test"); jobProps.setProperty(ConfigurationKeys.JOB_ID_KEY, "job_id_12345"); jobProps.setProperty(ConfigurationKeys.METRICS_ENABLED_KEY, "false"); Map<String, JobState.DatasetState> datasetStateMap = Maps.newHashMap(); for (int i = 0; i < 3; i++) { datasetStateMap.put(Integer.toString(i), new JobState.DatasetState()); } final BlockingQueue<ControllableCallable<Void>> callables = Queues.newLinkedBlockingQueue(); // There are three datasets, "0", "1", and "2", middle one will fail final JobContext jobContext = new ControllableCommitJobContext(jobProps, log, datasetStateMap, new Predicate<String>() { @Override public boolean apply(@Nullable String input) { return !input.equals("1"); } }, callables); ExecutorService executorService = Executors.newSingleThreadExecutor(); Future future = executorService.submit(new Runnable() { @Override public void run() { try { jobContext.commit(); } catch (IOException ioe) { throw new RuntimeException(ioe); } } }); // All three commits should be run (even though second one fails) callables.poll(1, TimeUnit.SECONDS).unblock(); callables.poll(1, TimeUnit.SECONDS).unblock(); callables.poll(1, TimeUnit.SECONDS).unblock(); try { // check future is done future.get(1, TimeUnit.SECONDS); Assert.fail(); } catch (ExecutionException ee) { // future should fail } // job failed Assert.assertEquals(jobContext.getJobState().getState(), JobState.RunningState.FAILED); } /** * A {@link Callable} that blocks until a different thread calls {@link #unblock()}. */ private class ControllableCallable<T> implements Callable<T> { private final BlockingQueue<Boolean> queue; private final Either<T, Exception> toReturn; private final String name; public ControllableCallable(Either<T, Exception> toReturn, String name) { this.queue = Queues.newArrayBlockingQueue(1); this.queue.add(true); this.toReturn = toReturn; this.name = name; } public void unblock() { if (!this.queue.isEmpty()) { this.queue.poll(); } } @Override public T call() throws Exception { this.queue.put(false); if (this.toReturn instanceof Either.Left) { return ((Either.Left<T, Exception>) this.toReturn).getLeft(); } else { throw ((Either.Right<T, Exception>) this.toReturn).getRight(); } } } private class ControllableCommitJobContext extends DummyJobContext { private final Predicate<String> successPredicate; private final Queue<ControllableCallable<Void>> callablesQueue; public ControllableCommitJobContext(Properties jobProps, Logger logger, Map<String, JobState.DatasetState> datasetStateMap, Predicate<String> successPredicate, Queue<ControllableCallable<Void>> callablesQueue) throws Exception { super(jobProps, logger, datasetStateMap); this.successPredicate = successPredicate; this.callablesQueue = callablesQueue; } @Override protected Callable<Void> createSafeDatasetCommit(boolean shouldCommitDataInJob, boolean isJobCancelled, DeliverySemantics deliverySemantics, String datasetUrn, JobState.DatasetState datasetState, boolean isMultithreaded, JobContext jobContext) { ControllableCallable<Void> callable; if (this.successPredicate.apply(datasetUrn)) { callable = new ControllableCallable<>(Either.<Void, Exception>left(null), datasetUrn); } else { callable = new ControllableCallable<>(Either.<Void, Exception>right(new RuntimeException("Fail!")), datasetUrn); } this.callablesQueue.add(callable); return callable; } } }