diff options
author | dhanji <dhanji@d779f126-a31b-0410-b53b-1d3aecad763e> | 2010-09-16 05:32:53 +0000 |
---|---|---|
committer | dhanji <dhanji@d779f126-a31b-0410-b53b-1d3aecad763e> | 2010-09-16 05:32:53 +0000 |
commit | 65888c01862049c0f7744cf4dfac371ce780fb24 (patch) | |
tree | 3c55edc5aec038758f5a1b37f1d7679e558a312a /extensions/service | |
parent | 190e95c007d434c2c2ceae18fa3efc7d4887c0d2 (diff) | |
download | guice-65888c01862049c0f7744cf4dfac371ce780fb24.tar.gz |
Service API extension initial checkin. Support for basic start/stop lifecycle and parallelizing service startup. Needs more integration tests.
git-svn-id: https://google-guice.googlecode.com/svn/trunk@1236 d779f126-a31b-0410-b53b-1d3aecad763e
Diffstat (limited to 'extensions/service')
5 files changed, 403 insertions, 0 deletions
diff --git a/extensions/service/service.iml b/extensions/service/service.iml new file mode 100644 index 00000000..ee3fb50c --- /dev/null +++ b/extensions/service/service.iml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="JAVA_MODULE" version="4"> + <component name="NewModuleRootManager" inherit-compiler-output="true"> + <exclude-output /> + <content url="file://$MODULE_DIR$"> + <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" /> + </content> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + <orderEntry type="module" module-name="guice" /> + </component> +</module> + diff --git a/extensions/service/src/com/google/inject/service/AsyncService.java b/extensions/service/src/com/google/inject/service/AsyncService.java new file mode 100644 index 00000000..8672c0be --- /dev/null +++ b/extensions/service/src/com/google/inject/service/AsyncService.java @@ -0,0 +1,131 @@ +/** + * Copyright (C) 2010 Google Inc. + * + * 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 com.google.inject.service; + +import com.google.inject.internal.util.Preconditions; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; + +/** + * An asynchronous implementation of {@link com.google.inject.service.Service} + * that provides convenience callbacks to create your own services. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public abstract class AsyncService implements Service { + private final ExecutorService executor; + + /** + * A runnable that does nothing. + */ + private static final Runnable NOOP = new Runnable() { + public void run() { } + }; + + private volatile State state; + + public AsyncService(ExecutorService executor) { + this.executor = executor; + } + + public synchronized final Future<State> start() { + Preconditions.checkState(state != State.STOPPED, + "Cannot restart a service that has been stopped"); + + // Starts are idempotent. + if (state == State.STARTED) { + return new FutureTask<State>(NOOP, State.STARTED); + } + + final Future<State> task = executor.submit(new Callable<State>() { + public State call() { + onStart(); + return state = State.STARTED; + } + }); + + return task; + // Wrap it in another future to catch failures. +// return new FutureTask<State>(futureGet(task)); + } + + /** + * Called back when this service must do its start work. Typically occurs + * in a background thread. The result of this method is returned to the + * original caller of {@link Service#start()} and can thus be used to + * return a status message after start completes (or fails as the case + * may be). + */ + protected abstract void onStart(); + + public synchronized final Future<State> stop() { + Preconditions.checkState(state != null, "Must start this service before you stop it!"); + + // Likewise, stops are idempotent. + if (state == State.STOPPED) { + return new FutureTask<State>(NOOP, State.STOPPED); + } + + // TODO Should we bother doing the wrap, or is it enough to return + // this future as is? + final Future<State> task = executor.submit(new Callable<State>() { + public State call() { + onStop(); + return state = State.STOPPED; + } + }); + + // Wrap it in another future to catch failures. + return task; +// return new FutureTask<State>(futureGet(task)); + } + + /** + * Called back when this service must shutdown. Typically occurs + * in a background thread. The result of this method is returned to the + * original caller of {@link Service#stop()} and can thus be used to + * return a status message after stop completes (or fails as the case + * may be). + */ + protected abstract void onStop(); + + public final State state() { + return state; + } + + /** + * Returns a runnable that when run will get() the given future and + * update {@link #state} to FAILED if there was an exception thrown. + */ + private Callable<State> futureGet(final Future<State> task) { + return new Callable<State>() { + public State call() { + try { + System.out.println("FutureGEtting"); + return task.get(); + } catch (InterruptedException e) { + return state = State.FAILED; + } catch (ExecutionException e) { + return state = State.FAILED; + } + } + }; + } +} diff --git a/extensions/service/src/com/google/inject/service/CompositeService.java b/extensions/service/src/com/google/inject/service/CompositeService.java new file mode 100644 index 00000000..8e578c6a --- /dev/null +++ b/extensions/service/src/com/google/inject/service/CompositeService.java @@ -0,0 +1,125 @@ +/** + * Copyright (C) 2010 Google Inc. + * + * 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 com.google.inject.service; + +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.internal.util.ImmutableList; +import com.google.inject.internal.util.Lists; +import com.google.inject.internal.util.Preconditions; +import com.google.inject.internal.util.Sets; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; + +/** + * A service that composes other services together in a fixed order. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public class CompositeService { + private final Injector injector; + + private final Set<Key<? extends Service>> services = Sets.newLinkedHashSet(); + + /** + * Represents the state of this composite service. Will equal FAILED + * even if only one component service fails to start or stop. In other + * words, all component services must start successfully for this + * service to be considered started and similarly for stopped. + */ + private volatile Service.State compositeState; + private boolean composed; + + @Inject + CompositeService(Injector injector) { + this.injector = injector; + } + + public CompositeService add(Class<? extends Service> service) { + return add(Key.get(service)); + } + + public CompositeService add(Key<? extends Service> service) { + Preconditions.checkState(!composed, + "Cannot reuse a CompositeService after it has been compose()d. Please create a new one."); + // Verify that the binding exists. Throws an exception if not. + injector.getBinding(service); + + services.add(service); + return this; + } + + public Service compose() { + Preconditions.checkState(!composed, + "Cannot reuse a CompositeService after it has been compose()d. Please create a new one."); + composed = true; + + // Defensive copy. + final List<Key<? extends Service>> services = ImmutableList.copyOf(this.services); + + return new Service() { + public Future<State> start() { + final List<Future<State>> tasks = Lists.newArrayList(); + for (Key<? extends Service> service : services) { + tasks.add(injector.getInstance(service).start()); + } + + return futureGet(tasks, State.STARTED); + } + + public Future<State> stop() { + final List<Future<State>> tasks = Lists.newArrayList(); + for (Key<? extends Service> service : services) { + tasks.add(injector.getInstance(service).stop()); + } + + return futureGet(tasks, State.STOPPED); + } + + public State state() { + return compositeState; + } + }; + } + + private FutureTask<Service.State> futureGet(final List<Future<Service.State>> tasks, + final Service.State state) { + return new FutureTask<Service.State>(new Callable<Service.State>() { + public Service.State call() { + System.out.println("GeT :--- "); + boolean ok = true; + for (Future<Service.State> task : tasks) { + try { + System.out.println("GeT : " + task); + ok = state == task.get(); + System.out.println("OK : " + task); + } catch (InterruptedException e) { + return compositeState = Service.State.FAILED; + } catch (ExecutionException e) { + return compositeState = Service.State.FAILED; + } + } + + return compositeState = ok ? state : Service.State.FAILED; + } + }); + } +} diff --git a/extensions/service/src/com/google/inject/service/Service.java b/extensions/service/src/com/google/inject/service/Service.java new file mode 100644 index 00000000..fb7dc0d9 --- /dev/null +++ b/extensions/service/src/com/google/inject/service/Service.java @@ -0,0 +1,67 @@ +/** + * Copyright (C) 2010 Google Inc. + * + * 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 com.google.inject.service; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +/** + * An object with an operational state, asynchronous {@link #start()} and + * {@link #stop()} lifecycle methods to transition in and out of this state. + * Example services include http servers, RPC systems and timer tasks. + * + * @author dhanji@gmail.com (Dhanji R. Prasanna) + */ +public interface Service { + /** + * If the service has already been started, this method returns + * immediately without taking action. A stopped service may not be restarted. + * + * @return a future for the startup result, regardless of whether this call + * initiated startup. Calling {@link Future#get} will block until the + * service has finished starting, and returns the resultant state. If + * the service fails to start, {@link Future#get} will throw an {@link + * ExecutionException}. If it has already finished starting, + * {@link Future#get} returns immediately. + */ + Future<State> start(); + + /** + * If the service is {@link State#STARTED} initiates service shutdown and + * returns immediately. If the service has already been stopped, this + * method returns immediately without taking action. + * + * @return a future for the shutdown result, regardless of whether this call + * initiated shutdown. Calling {@link Future#get} will block until the + * service has finished shutting down, and either returns {@link + * State#STOPPED} or throws an {@link ExecutionException}. If it has + * already finished stopping, {@link Future#get} returns immediately. + */ + Future<State> stop(); + + /** + * Returns the current state of this service. One of {@link State} possible + * values, or null if this is a brand new object, i.e., has not been put into + * any state yet. + */ + State state(); + + /** + * The lifecycle states of a service. + */ + enum State { STARTED, STOPPED, FAILED } +} diff --git a/extensions/service/test/com/google/inject/service/SingleServiceIntegrationTest.java b/extensions/service/test/com/google/inject/service/SingleServiceIntegrationTest.java new file mode 100644 index 00000000..27a9baf6 --- /dev/null +++ b/extensions/service/test/com/google/inject/service/SingleServiceIntegrationTest.java @@ -0,0 +1,66 @@ +package com.google.inject.service; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import junit.framework.TestCase; + +/** + * Tests using Async Service. + */ +public class SingleServiceIntegrationTest extends TestCase { + + public final void testAsyncServiceLifecycle() throws InterruptedException { + ExecutorService executor = Executors.newSingleThreadExecutor(); + + final CountDownLatch latch = new CountDownLatch(2); + AsyncService service = new AsyncService(executor) { + @Override protected void onStart() { + assertEquals(2, latch.getCount()); + + latch.countDown(); + } + + @Override protected void onStop() { + assertEquals(1, latch.getCount()); + + latch.countDown(); + } + }; + + service.start(); + latch.await(2, TimeUnit.SECONDS); + + service.stop(); + latch.await(2, TimeUnit.SECONDS); + + executor.shutdown(); + assertEquals(0, latch.getCount()); + } + + public final void testAsyncServiceBlockingLifecycle() + throws InterruptedException, ExecutionException, TimeoutException { + ExecutorService executor = Executors.newSingleThreadExecutor(); + + final AtomicInteger integer = new AtomicInteger(2); + AsyncService service = new AsyncService(executor) { + @Override protected void onStart() { + assertEquals(2, integer.getAndDecrement()); + } + + @Override protected void onStop() { + assertEquals(1, integer.getAndDecrement()); + } + }; + + service.start().get(2, TimeUnit.SECONDS); + service.stop().get(2, TimeUnit.SECONDS); + + executor.shutdown(); + assertEquals(0, integer.get()); + } +} |