// =================================================================================================
// Copyright 2012 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.zookeeper.guice;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.net.InetSocketAddress;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import javax.annotation.Nullable;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.Atomics;
import com.google.inject.BindingAnnotation;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.Provides;
import com.google.inject.Singleton;
import com.google.inject.TypeLiteral;
import com.twitter.common.application.ShutdownRegistry;
import com.twitter.common.application.modules.LifecycleModule;
import com.twitter.common.application.modules.LocalServiceRegistry;
import com.twitter.common.args.Arg;
import com.twitter.common.args.CmdLine;
import com.twitter.common.args.constraints.NotEmpty;
import com.twitter.common.args.constraints.NotNull;
import com.twitter.common.base.Command;
import com.twitter.common.base.ExceptionalCommand;
import com.twitter.common.base.Supplier;
import com.twitter.common.zookeeper.Group.JoinException;
import com.twitter.common.zookeeper.ServerSet;
import com.twitter.common.zookeeper.ServerSet.EndpointStatus;
import com.twitter.common.zookeeper.ServerSet.UpdateException;
import com.twitter.common.zookeeper.ServerSetImpl;
import com.twitter.common.zookeeper.ZooKeeperClient;
import com.twitter.thrift.Status;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* A module that registers all ports in the {@link LocalServiceRegistry} in an {@link ServerSet}.
*
* Required bindings:
* <ul>
* <li> {@link ZooKeeperClient}
* <li> {@link ShutdownRegistry}
* <li> {@link LocalServiceRegistry}
* </ul>
*
* {@link LifecycleModule} must also be included by users so a startup action may be registered.
*
* Provided bindings:
* <ul>
* <li> {@link Supplier<EndpointStatus>}
* </ul>
*
* @author William Farner
*/
public class ServerSetModule extends AbstractModule {
/**
* BindingAnnotation for defaults to use in the service instance node.
*/
@BindingAnnotation @Target({ PARAMETER, METHOD, FIELD }) @Retention(RUNTIME)
private @interface Default { }
@NotNull
@NotEmpty
@CmdLine(name = "serverset_path", help = "ServerSet registration path")
protected static final Arg<String> SERVERSET_PATH = Arg.create(null);
@CmdLine(name = "aux_port_as_primary",
help = "Name of the auxiliary port to use as the primary port in the server set."
+ " This may only be used when no other primary port is specified.")
private static final Arg<String> AUX_PORT_AS_PRIMARY = Arg.create(null);
private static final Logger LOG = Logger.getLogger(ServerSetModule.class.getName());
private final Status initialStatus;
private final Optional<String> auxPortAsPrimary;
/**
* Calls {@link #ServerSetModule(Optional)} with an absent value.
*/
public ServerSetModule() {
this(Optional.<String>absent());
}
/**
* Calls {@link #ServerSetModule(Status, Optional)} with initial status {@link Status#ALIVE}.
*
* @param auxPortAsPrimary Name of the auxiliary port to use as the primary port.
*/
public ServerSetModule(Optional<String> auxPortAsPrimary) {
this(Status.ALIVE, auxPortAsPrimary);
}
/**
* Constructs a ServerSetModule that registers a startup action that registers this process in
* ZooKeeper, with the specified initial Status.
*
* @param initialStatus initial Status to report to ZooKeeper.
*/
public ServerSetModule(Status initialStatus) {
this(initialStatus, Optional.<String>absent());
}
/**
* Constructs a ServerSetModule that registers a startup action to register this process in
* ZooKeeper, with the specified initial status and auxiliary port to represent as the primary
* service port.
*
* @param initialStatus initial Status to report to ZooKeeper.
* @param auxPortAsPrimary Name of the auxiliary port to use as the primary port.
*/
public ServerSetModule(Status initialStatus, Optional<String> auxPortAsPrimary) {
this.initialStatus = Preconditions.checkNotNull(initialStatus);
this.auxPortAsPrimary = Preconditions.checkNotNull(auxPortAsPrimary);
}
@Override
protected void configure() {
requireBinding(ZooKeeperClient.class);
requireBinding(ShutdownRegistry.class);
requireBinding(LocalServiceRegistry.class);
LifecycleModule.bindStartupAction(binder(), ServerSetJoiner.class);
bind(new TypeLiteral<Supplier<EndpointStatus>>() { }).to(EndpointSupplier.class);
bind(EndpointSupplier.class).in(Singleton.class);
bind(Status.class).annotatedWith(Default.class).toInstance(initialStatus);
Optional<String> primaryPortName;
if (AUX_PORT_AS_PRIMARY.hasAppliedValue()) {
primaryPortName = Optional.of(AUX_PORT_AS_PRIMARY.get());
} else {
primaryPortName = auxPortAsPrimary;
}
bind(new TypeLiteral<Optional<String>>() { }).annotatedWith(Default.class)
.toInstance(primaryPortName);
}
@Provides
@Singleton
ServerSet provideServerSet(ZooKeeperClient zkClient) {
return new ServerSetImpl(zkClient, SERVERSET_PATH.get());
}
static class EndpointSupplier implements Supplier<EndpointStatus> {
private final AtomicReference<EndpointStatus> reference = Atomics.newReference();
@Nullable
@Override public EndpointStatus get() {
return reference.get();
}
void set(EndpointStatus endpoint) {
reference.set(endpoint);
}
}
private static class ServerSetJoiner implements Command {
private final ServerSet serverSet;
private final LocalServiceRegistry serviceRegistry;
private final ShutdownRegistry shutdownRegistry;
private final EndpointSupplier endpointSupplier;
private final Status initialStatus;
private final Optional<String> auxPortAsPrimary;
@Inject
ServerSetJoiner(
ServerSet serverSet,
LocalServiceRegistry serviceRegistry,
ShutdownRegistry shutdownRegistry,
EndpointSupplier endpointSupplier,
@Default Status initialStatus,
@Default Optional<String> auxPortAsPrimary) {
this.serverSet = checkNotNull(serverSet);
this.serviceRegistry = checkNotNull(serviceRegistry);
this.shutdownRegistry = checkNotNull(shutdownRegistry);
this.endpointSupplier = checkNotNull(endpointSupplier);
this.initialStatus = checkNotNull(initialStatus);
this.auxPortAsPrimary = checkNotNull(auxPortAsPrimary);
}
@Override public void execute() {
Optional<InetSocketAddress> primarySocket = serviceRegistry.getPrimarySocket();
Map<String, InetSocketAddress> auxSockets = serviceRegistry.getAuxiliarySockets();
InetSocketAddress primary;
if (primarySocket.isPresent()) {
primary = primarySocket.get();
} else if (auxPortAsPrimary.isPresent()) {
primary = auxSockets.get(auxPortAsPrimary.get());
if (primary == null) {
throw new IllegalStateException("No auxiliary port named " + auxPortAsPrimary.get());
}
} else {
throw new IllegalStateException("No primary service registered with LocalServiceRegistry,"
+ " and -aux_port_as_primary was not specified.");
}
final EndpointStatus endpointStatus;
try {
endpointStatus = serverSet.join(primary, auxSockets, initialStatus);
endpointSupplier.set(endpointStatus);
} catch (JoinException e) {
LOG.log(Level.WARNING, "Failed to join ServerSet.", e);
throw new RuntimeException(e);
} catch (InterruptedException e) {
LOG.log(Level.WARNING, "Interrupted while joining ServerSet.", e);
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
shutdownRegistry.addAction(new ExceptionalCommand<UpdateException>() {
@Override public void execute() throws UpdateException {
LOG.info("Leaving ServerSet.");
endpointStatus.update(Status.DEAD);
}
});
}
}
}