Fix Connectables connecting in the wrong order

This commit is contained in:
Jedediah Smith 2017-02-06 07:05:31 -05:00
parent 8863e72fba
commit 34b9d4f246
16 changed files with 95 additions and 101 deletions

View File

@ -1,12 +0,0 @@
package tc.oc.api.annotations;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Generic annotation for something that requires an API connection,
* and should be ommitted or cause an error if not connected.
*
* (maybe we should make that distinction explicit?)
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiRequired {}

View File

@ -2,16 +2,21 @@ package tc.oc.api.connectable;
import java.io.IOException;
import com.google.inject.binder.ScopedBindingBuilder;
import tc.oc.minecraft.api.event.Activatable;
import tc.oc.commons.core.plugin.PluginFacet;
/**
* Service that needs to be connected and disconnected along with the API.
* Service that needs to be connected and disconnected along with the API
*
* Use a {@link ConnectableBinder} to register these.
* Registration happens automatically the first time any {@link Connectable}
* instance is provisioned through Guice. If this happens before or during
* the connection process, the instance will be connected in the same order
* that it was provisioned, with respect to other {@link Connectable}s.
*
* TODO: This should probably extend {@link PluginFacet},
* but to do that, API needs to be able to find the services bound in other plugins.
* If a new {@link Connectable} instance is provisioned after the connection
* phase is complete, an exception is thrown. To ensure that a {@link Connectable}
* is provisioned in time to be connected, it is usually scoped with
* {@link ScopedBindingBuilder#asEagerSingleton()}
*/
public interface Connectable extends Activatable {
default void connect() throws IOException {};

View File

@ -1,10 +0,0 @@
package tc.oc.api.connectable;
import com.google.inject.Binder;
import tc.oc.commons.core.inject.SetBinder;
public class ConnectableBinder extends SetBinder<Connectable> {
public ConnectableBinder(Binder binder) {
super(binder);
}
}

View File

@ -1,14 +1,21 @@
package tc.oc.api.connectable;
import javax.inject.Provider;
import tc.oc.commons.core.inject.HybridManifest;
import tc.oc.commons.core.plugin.PluginFacetBinder;
import tc.oc.minecraft.api.event.ListenerBinder;
public class ConnectablesManifest extends HybridManifest {
@Override
protected void configure() {
bindAndExpose(Connector.class);
new PluginFacetBinder(binder())
.add(Connector.class);
bind(Connector.class);
new ListenerBinder(binder())
.bindListener().to(Connector.class);
final Provider<Connector> connectorProvider = getProvider(Connector.class);
publicBinder().bindProvisionSubtypesOfListener(Connectable.class, provision -> {
connectorProvider.get().register(provision.provision());
});
}
}

View File

@ -1,6 +1,10 @@
package tc.oc.api.connectable;
import java.io.IOException;
import java.util.Collections;
import java.util.Deque;
import java.util.IdentityHashMap;
import java.util.LinkedList;
import java.util.Set;
import java.util.logging.Logger;
import javax.inject.Inject;
@ -8,28 +12,36 @@ import javax.inject.Singleton;
import tc.oc.commons.core.exception.ExceptionHandler;
import tc.oc.commons.core.logging.Loggers;
import tc.oc.commons.core.plugin.PluginFacet;
import tc.oc.commons.core.util.ExceptionUtils;
import tc.oc.minecraft.api.event.Enableable;
import static com.google.common.base.Preconditions.checkState;
import static tc.oc.commons.core.IterableUtils.reverseForEach;
import static tc.oc.commons.core.exception.LambdaExceptionUtils.rethrowConsumer;
@Singleton
public class Connector implements PluginFacet {
class Connector implements Enableable {
protected final Logger logger;
private final ExceptionHandler exceptionHandler;
private final Set<Connectable> services;
private boolean connected;
private final Set<Connectable> registered = Collections.newSetFromMap(new IdentityHashMap<>());
private final Deque<Connectable> pending = new LinkedList<>();
private final Deque<Connectable> connected = new LinkedList<>();
private boolean finishedConnecting;
@Inject
Connector(Loggers loggers, ExceptionHandler exceptionHandler, Set<Connectable> services) {
@Inject Connector(Loggers loggers, ExceptionHandler exceptionHandler) {
this.exceptionHandler = exceptionHandler;
this.services = services;
this.logger = loggers.get(getClass());
}
void register(Connectable connectable) {
if(registered.add(connectable)) {
if(finishedConnecting) {
throw new IllegalStateException("Tried to provision a " + Connectable.class.getSimpleName() +
" when already connected");
}
pending.add(connectable);
}
}
private void connect(Connectable service) throws IOException {
if(service.isActive()) {
logger.fine(() -> "Connecting " + service.getClass().getName());
@ -44,23 +56,25 @@ public class Connector implements PluginFacet {
}
}
public boolean isConnected() {
return connected;
}
@Override
public void enable() {
checkState(!connected, "already connected");
connected = true;
checkState(!finishedConnecting, "already connected");
logger.fine(() -> "Connecting all services");
ExceptionUtils.propagate(() -> services.forEach(rethrowConsumer(this::connect)));
for(;;) {
final Connectable connectable = pending.poll();
if(connectable == null) break;
ExceptionUtils.propagate(() -> connect(connectable));
connected.push(connectable);
}
finishedConnecting = true;
}
@Override
public void disable() {
checkState(connected, "not connected");
connected = false;
checkState(finishedConnecting, "not connected");
logger.fine(() -> "Disconnecting all services");
reverseForEach(services, service -> exceptionHandler.run(() -> disconnect(service)));
while(!connected.isEmpty()) {
exceptionHandler.run(() -> disconnect(connected.pop()));
}
}
}

View File

@ -4,7 +4,6 @@ import java.util.Map;
import java.util.Set;
import javax.inject.Singleton;
import com.google.inject.Binder;
import com.google.inject.Provides;
import com.google.inject.TypeLiteral;
import com.google.inject.binder.LinkedBindingBuilder;
@ -12,8 +11,8 @@ import com.google.inject.multibindings.Multibinder;
import com.google.inject.multibindings.OptionalBinder;
import tc.oc.api.docs.virtual.Model;
import tc.oc.api.docs.virtual.PartialModel;
import tc.oc.api.connectable.ConnectableBinder;
import tc.oc.api.queue.QueueQueryService;
import tc.oc.commons.core.inject.Binders;
import tc.oc.commons.core.inject.KeyedManifest;
import tc.oc.commons.core.inject.SingletonManifest;
import tc.oc.commons.core.reflect.ResolvableType;
@ -29,7 +28,7 @@ public class ModelBinder<M extends Model, P extends PartialModel> implements Mod
private final TypeLiteral<M> M;
private final TypeLiteral<P> P;
private final Binder binder;
private final Binders binder;
private final Multibinder<ModelMeta> metas;
private final OptionalBinder<QueryService<M>> queryServiceBinder;
private final OptionalBinder<UpdateService<P>> updateServiceBinder;
@ -53,7 +52,7 @@ public class ModelBinder<M extends Model, P extends PartialModel> implements Mod
}
private ModelBinder(ProtectedBinder protectedBinder, TypeLiteral<M> M, TypeLiteral<P> P) {
this.binder = protectedBinder.publicBinder();
this.binder = Binders.wrap(protectedBinder.publicBinder());
this.M = M;
this.P = P;
@ -68,7 +67,7 @@ public class ModelBinder<M extends Model, P extends PartialModel> implements Mod
}
public LinkedBindingBuilder<ModelStore<M>> bindStore() {
new ConnectableBinder(binder).addBinding().to(ModelStore(M));
binder.provisionEagerly(ModelStore(M));
new SuspendableBinder(binder).addBinding().to(ModelStore(M));
return storeBinder.setBinding();
}

View File

@ -164,7 +164,7 @@ public class QueueClient implements Connectable {
}
try {
this.channel.basicPublish(exchange.name(),
getChannel().basicPublish(exchange.name(),
publish.routingKey(),
publish.mandatory(),
publish.immediate(),
@ -220,16 +220,22 @@ public class QueueClient implements Connectable {
@Override
public void connect() throws IOException {
// create connection and channel
logger.info("Connecting to AMQP API at " + Joiners.onCommaSpace.join(config.getAddresses()));
this.connection = this.createConnectionFactory().newConnection(this.config.getAddresses().toArray(new Address[0]));
this.channel = this.connection.createChannel();
if(config.getAddresses().isEmpty()) {
logger.warning("Skipping AMQP connection because no addresses are configured");
} else {
logger.info("Connecting to AMQP API at " + Joiners.onCommaSpace.join(config.getAddresses()));
this.connection = this.createConnectionFactory().newConnection(this.config.getAddresses().toArray(new Address[0]));
this.channel = this.connection.createChannel();
}
}
@Override
public void disconnect() throws IOException {
ExecutorUtils.shutdownImpatiently(executorService, logger, SHUTDOWN_TIMEOUT);
this.channel.close();
this.connection.close();
if(channel != null) {
channel.close();
connection.close();
}
}
}

View File

@ -1,6 +1,5 @@
package tc.oc.api.queue;
import tc.oc.api.connectable.ConnectableBinder;
import tc.oc.api.message.MessageQueue;
import tc.oc.commons.core.inject.HybridManifest;
import tc.oc.minecraft.suspend.SuspendableBinder;
@ -12,24 +11,15 @@ public class QueueManifest extends HybridManifest {
bindAndExpose(QueueClientConfiguration.class)
.to(QueueClientConfigurationImpl.class);
bindAndExpose(QueueClient.class);
bindAndExpose(Exchange.Direct.class);
bindAndExpose(Exchange.Fanout.class);
bindAndExpose(Exchange.Topic.class);
bindAndExpose(PrimaryQueue.class);
bindAndExpose(QueueClient.class).asEagerSingleton();
bindAndExpose(Exchange.Direct.class).asEagerSingleton();
bindAndExpose(Exchange.Fanout.class).asEagerSingleton();
bindAndExpose(Exchange.Topic.class).asEagerSingleton();
bindAndExpose(PrimaryQueue.class).asEagerSingleton();
publicBinder().forOptional(MessageQueue.class)
.setBinding().to(PrimaryQueue.class);
// These will connect in the order listed here.
// TODO: figure out the order from their dependencies.
final ConnectableBinder services = new ConnectableBinder(publicBinder());
services.addBinding().to(QueueClient.class);
services.addBinding().to(Exchange.Direct.class);
services.addBinding().to(Exchange.Fanout.class);
services.addBinding().to(Exchange.Topic.class);
services.addBinding().to(PrimaryQueue.class);
final SuspendableBinder suspendables = new SuspendableBinder(publicBinder());
suspendables.addBinding().to(PrimaryQueue.class);
}

View File

@ -2,7 +2,6 @@ package tc.oc.api.minecraft;
import com.google.inject.Provides;
import tc.oc.api.config.ApiConfiguration;
import tc.oc.api.connectable.ConnectableBinder;
import tc.oc.api.docs.Server;
import tc.oc.api.docs.virtual.ServerDoc;
import tc.oc.api.minecraft.config.MinecraftApiConfiguration;
@ -24,9 +23,6 @@ public final class MinecraftApiManifest extends HybridManifest {
@Override
protected void configure() {
bind(ServerDoc.Identity.class).to(Server.class);
new ConnectableBinder(binder())
.addBinding().to(MinecraftServiceImpl.class);
}
@Provides Server localServer(MinecraftService minecraftService) {
@ -53,6 +49,6 @@ public final class MinecraftApiManifest extends HybridManifest {
bindAndExpose(MinecraftApiConfiguration.class).to(MinecraftApiConfigurationImpl.class);
bindAndExpose(MinecraftService.class).to(MinecraftServiceImpl.class);
bindAndExpose(MinecraftServiceImpl.class); // Needs to be exposed so it can be registered as a connectable service
bind(MinecraftServiceImpl.class).asEagerSingleton();
}
}

View File

@ -19,7 +19,6 @@ import net.md_5.bungee.api.chat.TranslatableComponent;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import tc.oc.api.annotations.ApiRequired;
import tc.oc.api.bukkit.users.BukkitUserStore;
import tc.oc.api.docs.PlayerId;
import tc.oc.api.docs.virtual.EngagementDoc;
@ -54,7 +53,6 @@ import tc.oc.pgm.victory.VictoryMatchModule;
/**
* Responsible for creating/updating {@link EngagementDoc}s.
*/
@ApiRequired
@ListenerScope(MatchScope.LOADED)
public class EngagementMatchModule extends MatchModule implements Listener {

View File

@ -5,7 +5,6 @@ import javax.inject.Inject;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import tc.oc.api.annotations.ApiRequired;
import tc.oc.api.docs.virtual.MapDoc;
import tc.oc.api.docs.virtual.MatchDoc;
import tc.oc.api.docs.virtual.ServerDoc;
@ -25,7 +24,6 @@ import tc.oc.pgm.match.MatchModule;
import tc.oc.pgm.match.MatchScope;
import tc.oc.pgm.teams.events.TeamResizeEvent;
@ApiRequired
@ListenerScope(MatchScope.LOADED)
public class MatchPublishingMatchModule extends MatchModule implements Listener {
class Update implements ServerDoc.MatchStatusUpdate {

View File

@ -9,7 +9,7 @@ import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import java.time.Instant;
import tc.oc.api.annotations.ApiRequired;
import tc.oc.api.bukkit.users.BukkitUserStore;
import tc.oc.api.docs.Participation;
import tc.oc.api.docs.Server;
@ -26,7 +26,6 @@ import tc.oc.pgm.match.MatchScope;
import tc.oc.pgm.match.Party;
import tc.oc.pgm.teams.Team;
@ApiRequired
@ListenerScope(MatchScope.LOADED)
public class ParticipationPublishingMatchModule extends MatchModule implements Listener {

View File

@ -7,8 +7,6 @@ import javax.inject.Provider;
import com.google.inject.TypeLiteral;
import org.bukkit.event.Listener;
import tc.oc.api.connectable.Connector;
import tc.oc.api.annotations.ApiRequired;
import tc.oc.commons.core.reflect.Types;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.match.MatchModule;
@ -27,15 +25,12 @@ import tc.oc.pgm.module.ModuleManifest;
*/
public abstract class MatchModuleManifest<M extends MatchModule> extends ModuleManifest<MatchModule, MatchScoped, MatchModuleContext, M> {
protected final boolean apiRequired;
protected MatchModuleManifest() {
this(null);
}
protected MatchModuleManifest(@Nullable TypeLiteral<M> type) {
super(type);
this.apiRequired = rawType.isAnnotationPresent(ApiRequired.class);
}
@Override
@ -49,13 +44,9 @@ public abstract class MatchModuleManifest<M extends MatchModule> extends ModuleM
}
@Inject protected Provider<Match> matchProvider;
@Inject private Connector connector;
@Override
protected final Optional<M> provisionModuleWithoutDependencies() throws ModuleLoadException {
if(apiRequired && !connector.isConnected()) {
return Optional.empty();
}
return provisionModuleWithoutFiltering()
.filter(MatchModule::shouldLoad);
}

View File

@ -10,7 +10,7 @@ import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import java.time.Duration;
import java.time.Instant;
import tc.oc.api.annotations.ApiRequired;
import tc.oc.api.docs.Server;
import tc.oc.api.docs.virtual.DeathDoc;
import tc.oc.api.model.BatchUpdater;
@ -37,7 +37,6 @@ import tc.oc.pgm.tracker.damage.SpleefInfo;
import tc.oc.pgm.tracker.damage.TNTInfo;
import tc.oc.pgm.tracker.damage.TrackerInfo;
@ApiRequired
@ListenerScope(MatchScope.LOADED)
public class DeathPublishingMatchModule extends MatchModule implements Listener {

View File

@ -4,7 +4,7 @@ import org.bukkit.Location;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import java.time.Instant;
import tc.oc.api.annotations.ApiRequired;
import tc.oc.api.docs.Objective;
import tc.oc.api.docs.Server;
import tc.oc.api.model.IdFactory;
@ -27,7 +27,6 @@ import tc.oc.pgm.wool.PlayerWoolPlaceEvent;
import javax.annotation.Nullable;
import javax.inject.Inject;
@ApiRequired
@ListenerScope(MatchScope.LOADED)
public class ObjectivePublishingMatchModule extends MatchModule implements Listener {

View File

@ -4,6 +4,7 @@ import java.lang.annotation.Annotation;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import javax.inject.Inject;
import com.google.inject.Binder;
import com.google.inject.Binding;
@ -21,6 +22,8 @@ import com.google.inject.spi.Elements;
import com.google.inject.spi.ProvisionListener;
import com.google.inject.spi.TypeEncounter;
import com.google.inject.spi.TypeListener;
import tc.oc.commons.core.reflect.ResolvableType;
import tc.oc.commons.core.reflect.TypeArgument;
import tc.oc.commons.core.reflect.TypeParameter;
import tc.oc.commons.core.reflect.TypeResolver;
import tc.oc.commons.core.reflect.Types;
@ -161,6 +164,18 @@ public interface Binders extends ForwardingBinder {
return membersInjector(TypeLiteral.get(type));
}
class EagerProvisioner<T> {
@Inject EagerProvisioner(T t) {}
}
default <T> void provisionEagerly(Key<T> key) {
bind(key.ofType(new ResolvableType<EagerProvisioner<T>>(){}.with(new TypeArgument<T>(key.getTypeLiteral()){})))
.asEagerSingleton();
}
default <T> void provisionEagerly(TypeLiteral<T> type) { provisionEagerly(Key.get(type)); }
default <T> void provisionEagerly(Class<T> type) { provisionEagerly(Key.get(type)); }
@Override
default Binders withSource(Object source) {
return wrap(ForwardingBinder.super.withSource(source));