// =================================================================================================
// 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 javax.annotation.Nullable;
import com.google.common.base.Optional;
import com.google.common.util.concurrent.Atomics;
import com.google.inject.AbstractModule;
import com.google.inject.BindingAnnotation;
import com.google.inject.Inject;
import com.google.inject.Key;
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.NotNegative;
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.thrift.Status;
import static com.google.common.base.Preconditions.checkNotNull;
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;
/**
* A module that registers all ports in the {@link LocalServiceRegistry} in an {@link ServerSet}.
* <p/>
* Required bindings:
* <ul>
* <li> {@link ServerSet}
* <li> {@link ShutdownRegistry}
* <li> {@link LocalServiceRegistry}
* </ul>
* <p/>
* {@link LifecycleModule} must also be included by users so a startup action may be registered.
* <p/>
* Provided bindings:
* <ul>
* <li> {@link Supplier}<{@link EndpointStatus}>
* </ul>
*/
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 {}
/**
* Binding annotation to give the ServerSetJoiner a fixed known ServerSet that is appropriate to
* {@link ServerSet#join} on.
*/
@BindingAnnotation @Target({METHOD, PARAMETER}) @Retention(RUNTIME)
private @interface Joinable {}
private static final Key<ServerSet> JOINABLE_SS = Key.get(ServerSet.class, Joinable.class);
@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);
@NotNegative
@CmdLine(name = "shard_id", help = "Shard ID for this application.")
private static final Arg<Integer> SHARD_ID = Arg.create();
private static final Logger LOG = Logger.getLogger(ServerSetModule.class.getName());
/**
* Builds a Module tht can be used to join a {@link ServerSet} with the ports configured in a
* {@link LocalServiceRegistry}.
*/
public static class Builder {
private Key<ServerSet> key = Key.get(ServerSet.class);
private Optional<String> auxPortAsPrimary = Optional.absent();
/**
* Sets the key of the ServerSet to join.
*
* @param key Key of the ServerSet to join.
* @return This builder for chaining calls.
*/
public Builder key(Key<ServerSet> key) {
this.key = key;
return this;
}
/**
* Allows joining an auxiliary port with the specified {@code name} as the primary port of the
* ServerSet.
*
* @param auxPortName The name of the auxiliary port to join as the primary ServerSet port.
* @return This builder for chaining calls.
*/
public Builder namedPrimaryPort(String auxPortName) {
this.auxPortAsPrimary = Optional.of(auxPortName);
return this;
}
/**
* Creates a Module that will register a startup action that joins a ServerSet when installed.
*
* @return A Module.
*/
public ServerSetModule build() {
return new ServerSetModule(key, auxPortAsPrimary);
}
}
/**
* Creates a builder that can be used to configure and create a ServerSetModule.
*
* @return A ServerSetModule builder.
*/
public static Builder builder() {
return new Builder();
}
private final Key<ServerSet> serverSetKey;
private final Optional<String> auxPortAsPrimary;
/**
* 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 serverSetKey The key the ServerSet to join is bound under.
* @param auxPortAsPrimary Name of the auxiliary port to use as the primary port.
*/
ServerSetModule(Key<ServerSet> serverSetKey, Optional<String> auxPortAsPrimary) {
this.serverSetKey = checkNotNull(serverSetKey);
this.auxPortAsPrimary = checkNotNull(auxPortAsPrimary);
}
@Override
protected void configure() {
requireBinding(serverSetKey);
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);
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);
bind(JOINABLE_SS).to(serverSetKey);
}
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 Optional<String> auxPortAsPrimary;
@Inject
ServerSetJoiner(
@Joinable ServerSet serverSet,
LocalServiceRegistry serviceRegistry,
ShutdownRegistry shutdownRegistry,
EndpointSupplier endpointSupplier,
@Default Optional<String> auxPortAsPrimary) {
this.serverSet = checkNotNull(serverSet);
this.serviceRegistry = checkNotNull(serviceRegistry);
this.shutdownRegistry = checkNotNull(shutdownRegistry);
this.endpointSupplier = checkNotNull(endpointSupplier);
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 {
if (SHARD_ID.hasAppliedValue()) {
endpointStatus = serverSet.join(primary, auxSockets, SHARD_ID.get());
} else {
endpointStatus = serverSet.join(primary, auxSockets);
}
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.leave();
}
});
}
}
}