@Test public void serverNotEnoughWorkerThreadsCausesBindTimerToCloseChannel() throws Exception { BlockThreadSmppServerHandler serverHandler0 = new BlockThreadSmppServerHandler(); SmppServerConfiguration configuration = createSmppServerConfiguration(); // permit up to 0.5 seconds to bind configuration.setBindTimeout(500); DefaultSmppServer server0 = new DefaultSmppServer(configuration, serverHandler0); server0.start(); try { // there is an issue without telling the server how many worker threads // to create beforehand with starvation only Runtime.getRuntime().availableProcessors() // worker threads are created by default!!! (yikes) int workersToStarveWith = Runtime.getRuntime().availableProcessors(); // initiate bind requests on all sessions we care about -- this should // technicaly "starve" the server of worker threads since they'll all // be blocked in a Thread.sleep for (int i = 0; i < workersToStarveWith; i++) { DefaultSmppClient client0 = new DefaultSmppClient(); SmppSessionConfiguration sessionConfig0 = createDefaultConfiguration(); sessionConfig0.setName("WorkerTest.Session." + i); // don't use default method of binding, connect the socket first DefaultSmppSession session0 = client0.doOpen(sessionConfig0, new DefaultSmppSessionHandler()); // try to bind and execute a bind request and wait for a bind response BaseBind bindRequest = client0.createBindRequest(sessionConfig0); try { // just send the request without caring if it succeeds session0.sendRequestPdu(bindRequest, 2000, false); } catch (SmppChannelException e) { System.out.println(e.getMessage()); } } // now try to bind normally -- since all previous workers are "starved" // this should fail to bind and the socket closed by the "BindTimer" DefaultSmppClient client0 = new DefaultSmppClient(); SmppSessionConfiguration sessionConfig0 = createDefaultConfiguration(); sessionConfig0.setName("WorkerTestChannelClosed.Session"); sessionConfig0.setBindTimeout(750); try { client0.bind(sessionConfig0); Assert.fail(); } catch (SmppTimeoutException e) { // the BindTimer should end up closing the connection since the // worker thread were "starved" logger.debug("Correctly received SmppChannelException during bind"); } } finally { Thread.sleep(10500); Assert.assertEquals(0, server0.getChannels().size()); Assert.assertEquals(3, server0.getCounters().getBindTimeouts()); server0.destroy(); } }
/** * Creates a new default SmppServer. Window monitoring and automatic expiration of requests will * be disabled with no monitorExecutors. A "cachedDaemonThreadPool" will be used for IO worker * threads. * * @param configuration The server configuration to create this server with * @param serverHandler The handler implementation for handling bind requests and * creating/destroying sessions. */ public DefaultSmppServer(SmppServerConfiguration configuration, SmppServerHandler serverHandler) { this( configuration, serverHandler, null, configuration.isNonBlockingSocketsEnabled() ? new NioEventLoopGroup() : new OioEventLoopGroup(), configuration.isNonBlockingSocketsEnabled() ? new NioEventLoopGroup() : new OioEventLoopGroup()); }
/** * Creates a new default SmppServer. * * @param configuration The server configuration to create this server with * @param serverHandler The handler implementation for handling bind requests and * creating/destroying sessions. * @param monitorExecutor The scheduled executor that all sessions will share to monitor * themselves and expire requests. If null monitoring will be disabled. * @param bossGroup Specify the EventLoopGroup to accept new connections and handle accepted * connections. The {@link EventLoopGroup} is used to handle all the events and IO for {@link * SocketChannel}. * @param workerGroup The {@link EventLoopGroup} is used to handle all the events and IO for * {@link Channel}. */ public DefaultSmppServer( final SmppServerConfiguration configuration, SmppServerHandler serverHandler, ScheduledExecutorService monitorExecutor, EventLoopGroup bossGroup, EventLoopGroup workerGroup) { this.configuration = configuration; // the same group we'll put every server channel this.channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); this.serverHandler = serverHandler; // tie the server bootstrap to this server socket channel factory this.serverBootstrap = new ServerBootstrap(); // a factory for creating channels (connections) if (configuration.isNonBlockingSocketsEnabled()) { this.serverBootstrap.channel(NioServerSocketChannel.class); } else { this.serverBootstrap.channel(OioServerSocketChannel.class); } this.bossGroup = bossGroup; this.workerGroup = workerGroup; this.serverBootstrap.group(this.bossGroup, this.workerGroup); // set options for the server socket that are useful this.serverBootstrap.option(ChannelOption.SO_REUSEADDR, configuration.isReuseAddress()); // we use the same default pipeline for all new channels - no need for a factory this.serverConnector = new SmppServerConnector(channels, this); this.serverBootstrap.childHandler( new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline() .addLast(SmppChannelConstants.PIPELINE_SERVER_CONNECTOR_NAME, serverConnector); } }); // a shared timer used to make sure new channels are bound within X milliseconds this.bindTimer = new Timer(configuration.getName() + "-BindTimer0", true); // NOTE: this would permit us to customize the "transcoding" context for a server if needed this.transcoder = new DefaultPduTranscoder(new DefaultPduTranscoderContext()); this.sessionIdSequence = new AtomicLong(0); this.monitorExecutor = monitorExecutor; this.counters = new DefaultSmppServerCounters(); if (configuration.isJmxEnabled()) { registerMBean(); } }
protected void destroySession(Long sessionId, DefaultSmppSession session) { // session destroyed, now pass it upstream counters.incrementSessionDestroyedAndGet(); decrementSessionSizeCounters(session); serverHandler.sessionDestroyed(sessionId, session); // unregister this session as an mbean if (configuration.isJmxEnabled()) { session.unregisterMBean( configuration.getJmxDomain() + ":type=" + configuration.getName() + "Sessions,name=" + sessionId); } }
private void registerMBean() { if (configuration == null) { return; } if (configuration.isJmxEnabled()) { // register the this queue manager as an mbean try { ObjectName name = new ObjectName(configuration.getJmxDomain() + ":name=" + configuration.getName()); ManagementFactory.getPlatformMBeanServer().registerMBean(this, name); } catch (Exception e) { // log the error, but don't throw an exception for this datasource logger.error("Unable to register DefaultSmppServerMXBean [{}]", configuration.getName(), e); } } }
@Override public void stop() { if (this.channels.size() > 0) { logger.info( "{} currently has [{}] open child channel(s) that will be closed as part of stop()", configuration.getName(), this.channels.size()); } // close all channels still open within this session "bootstrap" this.channels.close().awaitUninterruptibly(); // clean up all external resources if (this.serverChannel != null) { this.serverChannel.close().awaitUninterruptibly(); this.serverChannel = null; } logger.info("{} stopped on SMPP port [{}]", configuration.getName(), configuration.getPort()); }
protected BaseBindResp createBindResponse(BaseBind bindRequest, int statusCode) { BaseBindResp bindResponse = (BaseBindResp) bindRequest.createResponse(); bindResponse.setCommandStatus(statusCode); bindResponse.setSystemId(configuration.getSystemId()); // if the server supports an SMPP server version >= 3.4 AND the bind request // included an interface version >= 3.4, include an optional parameter with configured // sc_interface_version TLV if (configuration.getInterfaceVersion() >= SmppConstants.VERSION_3_4 && bindRequest.getInterfaceVersion() >= SmppConstants.VERSION_3_4) { Tlv scInterfaceVersion = new Tlv( SmppConstants.TAG_SC_INTERFACE_VERSION, new byte[] {configuration.getInterfaceVersion()}); bindResponse.addOptionalParameter(scInterfaceVersion); } return bindResponse; }
@Override public void destroy() { this.bindTimer.cancel(); stop(); // Shut down all event loops to terminate all threads. bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); try { // Wait until all threads are terminated. bossGroup.terminationFuture().sync(); workerGroup.terminationFuture().sync(); } catch (InterruptedException e) { // is ok } this.serverBootstrap = null; unregisterMBean(); logger.info("{} destroyed on SMPP port [{}]", configuration.getName(), configuration.getPort()); }
@Override public void start() throws SmppChannelException { if (isDestroyed()) { throw new SmppChannelException("Unable to start: server is destroyed"); } try { ChannelFuture f = this.serverBootstrap.bind(new InetSocketAddress(configuration.getPort())); // wait until the connection is made successfully boolean timeout = !f.await(configuration.getBindTimeout()); if (timeout || !f.isSuccess()) throw new SmppChannelException( "Can't bind to port " + configuration.getPort() + " after " + configuration.getBindTimeout() + " milliseconds"); logger.info("{} started on SMPP port [{}]", configuration.getName(), configuration.getPort()); serverChannel = f.channel(); } catch (ChannelException e) { throw new SmppChannelException(e.getMessage(), e); } catch (InterruptedException e) { throw new SmppChannelException(e.getMessage(), e); } }
public void setConfiguration() { this.configuration = new SmppServerConfiguration(); configuration.setPort(PORT); configuration.setMaxConnectionSize(10); configuration.setNonBlockingSocketsEnabled(true); configuration.setDefaultRequestExpiryTimeout(30000); configuration.setDefaultWindowMonitorInterval(15000); configuration.setDefaultWindowSize(5); configuration.setDefaultWindowWaitTimeout(configuration.getDefaultRequestExpiryTimeout()); configuration.setDefaultSessionCountersEnabled(true); configuration.setJmxEnabled(true); configuration.setReuseAddress(true); }
protected void createSession( Long sessionId, Channel channel, SmppSessionConfiguration config, BaseBindResp preparedBindResponse) throws SmppProcessingException { // NOTE: exactly one PDU (bind request) was read from the channel, we // now need to delegate permitting this bind request by calling a method // further upstream. Only after the server-side is completely ready to // start processing requests from this session, do we want to actually // return the bind response and start reading further requests -- we'll // initially block reading from the channel first -- this will be turned // back on via the "serverReady()" method call on the session object // make sure the channel is not being read/processed (until we flag we're ready later on) channel.config().setAutoRead(false); // auto negotiate the interface version in use based on the requested interface version byte interfaceVersion = this.autoNegotiateInterfaceVersion(config.getInterfaceVersion()); // create a new server session associated with this server DefaultSmppSession session = new DefaultSmppSession( SmppSession.Type.SERVER, config, channel, this, sessionId, preparedBindResponse, interfaceVersion, monitorExecutor); // replace name of thread used for renaming SmppSessionThreadRenamer threadRenamer = (SmppSessionThreadRenamer) channel.pipeline().get(SmppChannelConstants.PIPELINE_SESSION_THREAD_RENAMER_NAME); threadRenamer.setThreadName(config.getName()); // add a logging handler after the thread renamer SmppSessionLogger loggingHandler = new SmppSessionLogger( DefaultSmppSession.class.getCanonicalName(), config.getLoggingOptions()); channel .pipeline() .addAfter( SmppChannelConstants.PIPELINE_SESSION_THREAD_RENAMER_NAME, SmppChannelConstants.PIPELINE_SESSION_LOGGER_NAME, loggingHandler); // decoder in pipeline is ok (keep it) // create a new wrapper around a session to pass the pdu up the chain channel.pipeline().remove(SmppChannelConstants.PIPELINE_SESSION_WRAPPER_NAME); channel .pipeline() .addLast( SmppChannelConstants.PIPELINE_SESSION_WRAPPER_NAME, new SmppSessionWrapper(session)); // check if the # of channels exceeds maxConnections if (this.channels.size() > this.configuration.getMaxConnectionSize()) { logger.warn( "The current connection size [{}] exceeds the configured max connection size [{}]", this.channels.size(), this.configuration.getMaxConnectionSize()); } // session created, now pass it upstream counters.incrementSessionCreatedAndGet(); incrementSessionSizeCounters(session); this.serverHandler.sessionCreated(sessionId, session, preparedBindResponse); // register this session as an mbean if (configuration.isJmxEnabled()) { session.registerMBean( configuration.getJmxDomain() + ":type=" + configuration.getName() + "Sessions,name=" + sessionId); } }
public SmppServerConfiguration createSmppServerConfiguration() { SmppServerConfiguration configuration = new SmppServerConfiguration(); configuration.setPort(PORT); configuration.setSystemId("cloudhopper"); return configuration; }