All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.caliper.runner.server.ServerSocketService Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2013 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.caliper.runner.server;

import static com.google.common.base.Preconditions.checkState;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;

import com.google.caliper.bridge.OpenedSocket;
import com.google.caliper.runner.options.CaliperOptions;
import com.google.caliper.util.Uuids;
import com.google.common.base.Function;
import com.google.common.base.Supplier;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimaps;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.AbstractExecutionThreadService;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.Service;
import com.google.common.util.concurrent.SettableFuture;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.nio.channels.Channels;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.annotation.concurrent.GuardedBy;
import javax.inject.Inject;
import javax.inject.Singleton;

/**
 * A {@link Service} that manages a {@link ServerSocket}.
 *
 * 

This service provides two pieces of functionality: * *

    *
  1. It adapts {@link ServerSocket#accept()} to a {@link ListenableFuture} of an opened socket. *
  2. It demultiplexes incoming connections based on a UUID that is sent over the socket. *
* *

The {@linkplain State states} of this service are as follows: * *

    *
  • {@linkplain State#NEW NEW} : Idle state, the {@link ServerSocket} is not open yet. *
  • {@linkplain State#STARTING STARTING} : {@link ServerSocket} is opened *
  • {@linkplain State#RUNNING RUNNING} : We are continuously accepting and parsing connections * from the socket. *
  • {@linkplain State#STOPPING STOPPING} : The server socket is closing and all pending * connection requests are terminated, connection requests will fail immediately. *
  • {@linkplain State#TERMINATED TERMINATED} : Idle state, the socket is closed. *
  • {@linkplain State#FAILED FAILED} : The service will transition to failed if it encounters * any errors while accepting connections or reading from connections. *
* *

Note to future self. There have been a few attempts to make it so that it is no longer * necessary to dedicate a thread to this service (basically turn it into an AbstractIdleService). * The general idea has been to make callers to getConnection invoke accept, here is why it didn't * work. * *

    *
  • If you make getConnection a blocking method that calls accept until it finds the connection * with its id, then there is no way to deal with connections that never arrive. For example, * if the worker crashes before connecting then the thread calling accept will block forever * waiting for it. The only way to unblock a thread stuck on accept() is to close the socket * (this holds for ServerSocketChannels and normal ServerSockets), but we cannot do that in * this case because the socket is a shared resource. *
  • If you make getConnection a non-blocking, polling based method then you expose yourself to * potential deadlocks (due to missed signals) depending on what thread you poll from. If the * polling thread is any of the threads that are involved with processing messages from the * worker I believe there to be a deadlock risk. Basically, if the worker sends messages over * its output streams and then calls Socket.connect, and no printing to stdout or stderr * occurs while connecting. Then if the runner polls, but misses the connection and then tries * to read again, it will deadlock. *
*/ @Singleton public final class ServerSocketService extends AbstractExecutionThreadService { private enum Source { REQUEST, ACCEPT } private final Lock lock = new ReentrantLock(); /** * Contains futures that have either only been accepted or requested. Once both occur they are * removed from this map. */ @GuardedBy("lock") private final Map> halfFinishedConnections = Maps.newHashMap(); /** * Contains the history of connections so we can ensure that each id is only accepted once and * requested once. */ @GuardedBy("lock") private final SetMultimap connectionState = Multimaps.newSetMultimap( Maps.>newEnumMap(Source.class), new Supplier>() { @Override public Set get() { return Sets.newHashSet(); } }); private final CaliperOptions caliperOptions; private ServerSocket serverSocket; @Inject ServerSocketService(CaliperOptions caliperOptions) { this.caliperOptions = caliperOptions; } /** Gets the port this server is using. */ public int getPort() { awaitRunning(); checkState(serverSocket != null, "Socket has not been opened yet"); return serverSocket.getLocalPort(); } /** * Returns a {@link ListenableFuture} for an {@link OpenedSocket} corresponding to the given id. * *

N.B. calling this method 'consumes' the connection and as such calling it or {@link * #getInputStream} twice with the same id will not work; the second future returned will never * complete. Similarly calling it with an id that does not correspond to a worker trying to * connect will also fail. */ public ListenableFuture getConnection(UUID id) { return Futures.transform(getSocket(id), OPENED_SOCKET_FUNCTION, directExecutor()); } /** * Returns a {@link ListenableFuture} for an {@link InputStream} corresponding to the given id. * *

N.B. calling this method 'consumes' the connection and as such calling it or {@link * #getConnection} twice with the same id will not work; the second future returned will never * complete. Similarly calling it with an id that does not correspond to a worker trying to * connect will also fail. */ public ListenableFuture getInputStream(UUID id) { return Futures.transform(getSocket(id), INPUT_STREAM_FUNCTION, directExecutor()); } private ListenableFuture getSocket(UUID id) { checkState(isRunning(), "You can only get connections from a running service: %s", this); return getSocketImpl(id, Source.REQUEST); } @Override protected void startUp() throws Exception { serverSocket = new ServerSocket(caliperOptions.localPort()); } @Override protected void run() throws Exception { while (isRunning()) { Socket socket; try { socket = serverSocket.accept(); } catch (SocketException e) { // we were closed return; } UUID id = getId(socket); // N.B. you should not call set with the lock held, to prevent same thread executors from // running with the lock. getSocketImpl(id, Source.ACCEPT).set(socket); } } private UUID getId(Socket socket) throws IOException { return Uuids.readFromChannel(Channels.newChannel(socket.getInputStream())); } /** * Returns a {@link SettableFuture} from the map of connections. * *

This method has the following properties: * *

    *
  • If the id is present in {@link #connectionState}, this will throw an {@link * IllegalStateException}. *
  • The id and source are recorded in {@link #connectionState} *
  • If the future is already in {@link #halfFinishedConnections}, it is removed and returned. *
  • If the future is not in {@link #halfFinishedConnections}, a new {@link SettableFuture} is * added and then returned. *

    These features together ensure that each connection can only be accepted once, only * requested once and once both have happened it will be removed from {@link * #halfFinishedConnections}. */ private SettableFuture getSocketImpl(UUID id, Source source) { lock.lock(); try { checkState( connectionState.put(source, id), "Connection for %s has already been %s", id, source); SettableFuture future = halfFinishedConnections.get(id); if (future == null) { future = SettableFuture.create(); halfFinishedConnections.put(id, future); } else { halfFinishedConnections.remove(id); } return future; } finally { lock.unlock(); } } @Override protected void triggerShutdown() { try { serverSocket.close(); } catch (IOException e) { // best effort... } } @Override protected void shutDown() throws Exception { serverSocket.close(); // Now we have either been asked to stop or have failed with some kind of exception, we want to // notify all pending requests, so if there are any references outside of this class they will // notice. lock.lock(); try { for (SettableFuture future : halfFinishedConnections.values()) { future.setException(new Exception("The socket has been closed")); } halfFinishedConnections.clear(); connectionState.clear(); } finally { lock.unlock(); } } private static final Function OPENED_SOCKET_FUNCTION = new Function() { @Override public OpenedSocket apply(Socket socket) { try { return OpenedSocket.fromSocket(socket); } catch (IOException e) { throw new RuntimeException(e); } } }; private static final Function INPUT_STREAM_FUNCTION = new Function() { @Override public InputStream apply(Socket socket) { try { return socket.getInputStream(); } catch (IOException e) { throw new RuntimeException(e); } } }; }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy