// =================================================================================================
// Copyright 2011 Twitter, Inc.
// -------------------------------------------------------------------------------------------------
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this work except in compliance with the License.
// You may obtain a copy of the License in the LICENSE file, or 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.twitter.common.application.modules;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.inject.Inject;
import com.google.inject.Provider;
import org.apache.commons.lang.builder.ToStringBuilder;
import com.twitter.common.application.ShutdownRegistry;
import com.twitter.common.application.modules.LifecycleModule.LaunchException;
import com.twitter.common.application.modules.LifecycleModule.Service;
import com.twitter.common.application.modules.LifecycleModule.ServiceRunner;
import com.twitter.common.base.Command;
import com.twitter.common.net.InetSocketAddressHelper;
/**
* Registry for services that should be exported from the application.
*
* Example of announcing and registering a port:
* <pre>
* class MyLauncher implements Provider<LocalService> {
* public LocalService get() {
* // Launch service.
* }
* }
*
* class MyServiceModule extends AbstractModule {
* public void configure() {
* LifeCycleModule.bindServiceLauncher(binder(), MyLauncher.class);
* }
* }
* </pre>
*
* @author William Farner
*/
public class LocalServiceRegistry {
private static final Predicate<LocalService> IS_PRIMARY = new Predicate<LocalService>() {
@Override public boolean apply(LocalService service) {
return service.primary;
}
};
private static final Function<LocalService, InetSocketAddress> SERVICE_TO_SOCKET =
new Function<LocalService, InetSocketAddress>() {
@Override public InetSocketAddress apply(LocalService service) {
try {
return InetSocketAddressHelper.getLocalAddress(service.port);
} catch (UnknownHostException e) {
throw new RuntimeException("Failed to resolve local address for " + service, e);
}
}
};
private static final Function<LocalService, String> GET_NAME =
new Function<LocalService, String>() {
@Override public String apply(LocalService service) {
return service.name.get();
}
};
private final ShutdownRegistry shutdownRegistry;
private final Provider<Set<ServiceRunner>> runnerProvider;
private Optional<InetSocketAddress> primarySocket = null;
private Map<String, InetSocketAddress> auxiliarySockets = null;
/**
* Creates a new local service registry.
*
* @param runnerProvider provider of registered local services.
* @param shutdownRegistry Shutdown registry to tear down launched services.
*/
@Inject
public LocalServiceRegistry(@Service Provider<Set<ServiceRunner>> runnerProvider,
ShutdownRegistry shutdownRegistry) {
this.runnerProvider = Preconditions.checkNotNull(runnerProvider);
this.shutdownRegistry = Preconditions.checkNotNull(shutdownRegistry);
}
/**
* Launches the local services if not already launched, otherwise this is a no-op.
*/
void ensureLaunched() {
if (primarySocket == null) {
ImmutableList.Builder<LocalService> builder = ImmutableList.builder();
for (ServiceRunner runner : runnerProvider.get()) {
try {
LocalService service = runner.launch();
builder.add(service);
shutdownRegistry.addAction(service.shutdownCommand);
} catch (LaunchException e) {
throw new IllegalStateException("Failed to launch " + runner, e);
}
}
List<LocalService> localServices = builder.build();
Iterable<LocalService> primaries = Iterables.filter(localServices, IS_PRIMARY);
switch (Iterables.size(primaries)) {
case 0:
primarySocket = Optional.absent();
break;
case 1:
primarySocket = Optional.of(SERVICE_TO_SOCKET.apply(Iterables.getOnlyElement(primaries)));
break;
default:
throw new IllegalArgumentException("More than one primary local service: " + primaries);
}
Map<String, LocalService> byName;
try {
byName = Maps.uniqueIndex(
Iterables.filter(localServices, Predicates.not(IS_PRIMARY)), GET_NAME);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Auxiliary services with identical names.", e);
}
auxiliarySockets = ImmutableMap.copyOf(Maps.transformValues(byName, SERVICE_TO_SOCKET));
}
}
/**
* Gets the mapping from auxiliary port name to socket.
*
* @return Auxiliary port mapping.
*/
public synchronized Map<String, InetSocketAddress> getAuxiliarySockets() {
ensureLaunched();
return auxiliarySockets;
}
/**
* Gets the optional primary socket address, and returns an unresolved local socket address
* representing that port.
*
* @return Local socket address for the primary port.
* @throws IllegalStateException If the primary port was not set.
*/
public synchronized Optional<InetSocketAddress> getPrimarySocket() {
ensureLaunched();
return primarySocket;
}
/**
* An individual local service.
*/
public static final class LocalService {
private final boolean primary;
private final Optional<String> name;
private final int port;
private final Command shutdownCommand;
private LocalService(boolean primary, Optional<String> name, int port,
Command shutdownCommand) {
this.primary = primary;
this.name = name;
this.port = port;
this.shutdownCommand = shutdownCommand;
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("primary", primary)
.append("name", name)
.append("port", port)
.toString();
}
/**
* Creates a primary local service.
*
* @param port Service port.
* @param shutdownCommand A command that will shut down the service.
* @return A new primary local service.
*/
public static LocalService primaryService(int port, Command shutdownCommand) {
return new LocalService(true, Optional.<String>absent(), port, shutdownCommand);
}
/**
* Creates a named auxiliary service.
*
* @param name Service name.
* @param port Service port.
* @param shutdownCommand A command that will shut down the service.
* @return A new auxiliary local service.
*/
public static LocalService auxiliaryService(String name, int port, Command shutdownCommand) {
return new LocalService(false, Optional.of(name), port, shutdownCommand);
}
}
}