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

io.continual.onap.services.subscriber.OnapMsgRouterSubscriber Maven / Gradle / Ivy

There is a newer version: 0.9.3
Show newest version
package io.continual.onap.services.subscriber;

import java.io.IOException;
import java.net.Proxy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;

import io.continual.onap.services.mrCommon.Clock;
import io.continual.onap.services.mrCommon.CommonClientBuilder;
import io.continual.onap.services.mrCommon.HostSelector;
import io.continual.onap.services.mrCommon.HttpHelper;
import io.continual.onap.services.mrCommon.HttpHelper.Credentials;
import io.continual.onap.services.mrCommon.JsonResponseParser;
import io.continual.onap.services.mrCommon.SimpleJsonResponseParser;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

/**
 * A simple message router subscriber. This class issues HTTP transactions that execute in the foreground
 * to push messages to the ONAP Message Router service. 
 */
public class OnapMsgRouterSubscriber
{
	/**
	 * A builder for the subscriber.
	 */
	public static class Builder extends CommonClientBuilder
	{
		public static final int NO_RECV_LIMIT = -1;
		
		public Builder () {}

		/**
		 * Add a host to the set the publisher can use. If you do not provide a protocol,
		 * "http://" is assumed. You may specify "https://" or "http://". If you do not 
		 * provide a port (e.g. "host:3904"), then 3904 is assumed for http, and 3905
		 * for https.  Thus "localhost" is treated as "http://localhost:3904".
		 * 
		 * @param host the host to add to the host set
		 * @return this builder
		 */
		@Override
		public Builder withHost ( String host )
		{
			super.withHost ( host );
			return this;
		}

		/**
		 * Add each host to the host list. See withHost ( String host ) for details. 
		 * @param hosts a collection of hosts to add to the host set
		 * @return this builder
		 */
		@Override
		public Builder withHosts ( Collection hosts )
		{
			super.withHosts ( hosts );
			return this;
		}
		
		/**
		 * Add each host to the host list. See withHost ( String host ) for details. 
		 * @param hosts a collection of hosts to add to the host set
		 * @return this builder
		 */
		@Override
		public Builder withHosts ( String[] hosts )
		{
			super.withHosts ( hosts );
			return this;
		}

		/**
		 * Clear any hosts the builder already knows about.
		 * @return this builder
		 */
		@Override
		public Builder forgetHosts ()
		{
			super.forgetHosts ();
			return this;
		}

		/**
		 * Specify the topic to publish to.
		 * @param topic the topic on which to post messages
		 * @return this builder
		 */
		@Override
		public Builder onTopic ( String topic )
		{
			super.onTopic ( topic );
			return this;
		}

		/**
		 * Specify the subscriber group that this subscriber belongs to.
		 * @param group the subscriber group name
		 * @return this builder
		 */
		public Builder inGroup ( String group )
		{
			fSubGroup = group;
			return this;
		}
		
		/**
		 * Specify the subscriber id for this subscriber. This must be unique across members of the group.
		 * @param id the unique subscriber ID in the group
		 * @return this builder
		 */
		public Builder withSubscriberId ( String id )
		{
			fSubId = id;
			return this;
		}

		/**
		 * Specify the amount of time to wait at the server for messages. This is essentially a long-poll
		 * mechanism supported by the ONAP Message Router system.
		 * @param ms the number of milliseconds to wait at the server for messages.
		 * @return this builder
		 */
		public Builder waitAtServerAtMost ( long ms )
		{
			fServerWaitMs = ms;
			return this;
		}

		/**
		 * Specify the maximum number of events to return on a fetch.
		 * @param eventCount the number of events to return at most
		 * @return this builder
		 */
		public Builder recvAtMostEvents ( int eventCount )
		{
			fMaxEventsPerFetch = eventCount;
			return this;
		}

		/**
		 * Do not specify a maximum number of events to return on a fetch, allowing the
		 * server to determine the number to send.
		 * @return this builder
		 */
		public Builder noRecvLimit ()
		{
			return recvAtMostEvents ( NO_RECV_LIMIT );
		}

		/**
		 * Specify the amount of time to wait on a socket connection, read, or write.
		 * @param ms the number of milliseconds to wait for a socket operation (connect/read/write)
		 * @return this builder
		 */
		@Override
		public Builder socketWaitAtMost ( long ms )
		{
			super.socketWaitAtMost ( ms );
			return this;
		}

		/**
		 * Set HTTP basic auth credentials. If user is null, the auth info is removed from the builder.
		 * @param user the username for basic auth credentials
		 * @param pwd  the password for basic auth credentials
		 * @return this builder
		 */
		@Override
		public Builder asUser ( String user, String pwd )
		{
			super.asUser ( user, pwd );
			return this;
		}

		/**
		 * Set an API key and secret. If the API key is null, the auth info is removed from the builder.
		 * @param apiKey the API key for the user
		 * @param apiSecret the API key's secret
		 * @return this builder
		 */
		@Override
		public Builder withApiKey ( String apiKey, String apiSecret )
		{
			super.withApiKey ( apiKey, apiSecret );
			return this;
		}

		/**
		 * If no protocol is provided on a host string, default to http://
		 * @return this builder
		 */
		@Override
		public Builder defaultHttp ()
		{
			return defaultHttps ( false );
		}

		/**
		 * If no protocol is provided on a host string, default to https://
		 * @return this builder
		 */
		@Override
		public Builder defaultHttps ()
		{
			return defaultHttps ( true );
		}

		/**
		 * If no protocol is provided on a host string, default to https:// if true,
		 * http:// if false.
		 * @param https if true, use https. if false, use http
		 * @return this builder
		 */
		@Override
		public Builder defaultHttps ( boolean https )
		{
			super.defaultHttps ( https );
			return this;
		}

		/**
		 * Specify a clock to use within this implementation.
		 * @param clock the clock to use for timing
		 * @return this builder
		 */
		@Override
		public Builder withClock ( Clock clock )
		{
			super.withClock ( clock );
			return this;
		}

		/**
		 * Specify a proxy to use for the HTTP connection to Message Router.
		 * @param proxy a proxy string, which can optionally end in :port, e.g. proxy.example.com:8888
		 * @return this builder
		 */
		@Override
		public Builder usingProxy ( String proxy )
		{
			super.usingProxy ( proxy );
			return this;
		}

		/**
		 * Specify a proxy to use for the HTTP connection to Message Router.
		 * @param host a proxy host name
		 * @param port a port number
		 * @return this builder
		 */
		@Override
		public Builder usingProxy ( String host, int port )
		{
			super.usingProxy ( host, port );
			return this;
		}

		/**
		 * Provide a response parser.
		 * @param p the reponse parser
		 * @return this builder
		 */
		public Builder parseWith ( JsonResponseParser p )
		{
			fResponseParser = p;
			return this;
		}

		/**
		 * Build the subscriber given this specification.
		 * @return a new subscriber
		 */
		public OnapMsgRouterSubscriber build ()
		{
			return new OnapMsgRouterSubscriber ( this );
		}

		private String fSubGroup = null;
		private String fSubId = null;
		private long fServerWaitMs = 15000L;	// proactively make sure this caller does something reasonable
		private int fMaxEventsPerFetch = NO_RECV_LIMIT;
		private JsonResponseParser fResponseParser = new SimpleJsonResponseParser ();
	}

	/**
	 * Get a new builder
	 * 
	 * @return a builder
	 */
	public static Builder builder ()
	{
		return new Builder ();
	}

	@Override
	public String toString ()
	{
		return fLabel;
	}

	public OnapMrFetchResponse fetch ( )
	{
		return fetch ( -1L, -1 );
	}

	public OnapMrFetchResponse fetch ( long waitAtServerMs, int maxEventsToFetch )
	{
		// if not specified explicitly in this call, use instance settings
		if ( waitAtServerMs < 0L )
		{
			waitAtServerMs = fServerWaitMs;
		}
		if ( maxEventsToFetch < 0 )
		{
			maxEventsToFetch = fMaxEventsPerFetch;
		}
		
		// read from MR, trying each host in order until we have a conclusion...

		final ArrayList hostsLeft = new ArrayList<> ();
		fHosts.copyInto ( hostsLeft );

		final long noResponseTimeoutMs = fClock.nowMs () + fSocketWaitTimeoutMs;
		while ( fClock.nowMs () < noResponseTimeoutMs && hostsLeft.size () > 0 )
		{
			final String host = hostsLeft.remove ( 0 );
			final String path = buildPath ( host, waitAtServerMs, maxEventsToFetch );

			final Request.Builder reqBuilder = new Request.Builder ()
				.url ( path )
				.get ()
			;
			HttpHelper.addAuth ( reqBuilder, fCreds, fClock );
			final Request req = reqBuilder.build ();

			fLog.info ( "GET {} ({})", path, fCreds.getUserDescription () );

			final long trxStartMs = fClock.nowMs ();
			try ( Response response = fHttpClient.newCall ( req ).execute () )
			{
				final long trxEndMs = fClock.nowMs ();
				final long trxDurationMs = trxEndMs - trxStartMs;

				final int statusCode = response.code ();
				final String statusText = response.message ();

				fLog.info ( "    MR reply {} {} ({} ms)", statusCode, statusText, trxDurationMs );

				if ( HttpHelper.isSuccess ( statusCode ) )
				{
					// process the response body into strings
					final OnapMrFetchResponse fetchResponse = new OnapMrFetchResponse ( statusCode, statusText );
					fResponseParser.parseResponseBody ( response.body (), fetchResponse );
					return fetchResponse;
				}
				else if ( HttpHelper.isClientFailure ( statusCode ) )
				{
					// just relay MR's reply
					return new OnapMrFetchResponse ( statusCode, statusText, new LinkedList () );
				}
				else if ( HttpHelper.isServerFailure ( statusCode ) )
				{
					// that host has a problem, move on
					fHosts.demote ( host );
				}
			}
			catch ( IOException x )
			{
				final long trxEndMs = fClock.nowMs ();
				final long trxDurationMs = trxEndMs - trxStartMs;

				fLog.warn ( "    MR failure for host [{}]: {} ({} ms)", host, x.getMessage (), trxDurationMs );
				fHosts.demote ( host );
			}
			catch ( Throwable t )
			{
				fLog.warn ( "    Throwable", t.getMessage (), t );
				throw t;
			}
		}

		// if we're here, we've timed out on all MR hosts and we have to fail the transaction.
		return new OnapMrFetchResponse ( HttpHelper.k503_serviceUnavailable, "No Message Router server could acknowledge the request.", new LinkedList () );
	}

	/**
	 * Build a URL path for Message Router, provided protocol, port, and path as needed
	 * @param host
	 * @return a complete URL path
	 */
	private String buildPath ( String host, long waitAtServerMs, int maxEventsToFetch )
	{
		final StringBuilder sb = new StringBuilder ();

		// add a protocol if one is not provided
		if ( !host.contains ( "://" ) )
		{
			sb.append ( fDefaultHttps ? "https://" : "http://" );
		}

		// add the host
		sb.append ( host );

		// add a port if necessary
		if ( !host.contains ( ":" ) )
		{
			sb.append ( host.startsWith ( "https://" ) ? ":3905" : ":3904" );
		}

		// finally the path parts
		sb
			.append ( "/events/" )
			.append ( HttpHelper.urlEncode ( fTopic ) )
			.append ( "/" )
			.append ( HttpHelper.urlEncode ( fSubGroup ) )
			.append ( "/" )
			.append ( HttpHelper.urlEncode ( fSubId ) )
		;

		boolean argsAdded = false;

		if ( waitAtServerMs > -1L )
		{
			sb
				.append ( argsAdded ? "&" : "?" )
				.append ( "timeout=" )
				.append ( waitAtServerMs )
			;
			argsAdded = true;
		}

		if ( maxEventsToFetch > -1L )
		{
			sb
				.append ( argsAdded ? "&" : "?" )
				.append ( "limit=" )
				.append ( maxEventsToFetch )
			;
			argsAdded = true;
		}

		return sb.toString ();
	}

	Clock getClock () { return fClock; }

	private final HostSelector fHosts;
	private final String fTopic;
	private final String fSubGroup;
	private final String fSubId;
	private final long fServerWaitMs;
	private final int fMaxEventsPerFetch;
	private final long fSocketWaitTimeoutMs;
	private final boolean fDefaultHttps;
	private final Credentials fCreds;
	private final String fLabel;
	private final Clock fClock;

	private final OkHttpClient fHttpClient;
	private final JsonResponseParser fResponseParser;

	private final Logger fLog;

	private OnapMsgRouterSubscriber ( Builder builder )
	{
		if ( builder.getHosts().size () < 1 ) throw new IllegalArgumentException ( "No hosts provided." );

		fHosts = HostSelector.builder ()
			.withHosts ( builder.getHosts () )
			.build ()
		;
		fDefaultHttps = builder.getDefaultHttps ();

		fCreds = builder.getCredentials ();
		if ( fCreds == null ) throw new IllegalArgumentException ( "No credentials instance provided." );

		fTopic = builder.getTopic();
		if ( fTopic == null || fTopic.length () < 1 ) throw new IllegalArgumentException ( "No topic provided." );

		fSubGroup = builder.fSubGroup;
		if ( fSubGroup == null || fSubGroup.length () < 1 ) throw new IllegalArgumentException ( "No subscription group provided." );

		fSubId = builder.fSubId == null ? UUID.randomUUID ().toString () : builder.fSubId;

		fServerWaitMs = builder.fServerWaitMs;
		fMaxEventsPerFetch = builder.fMaxEventsPerFetch;
	
		fSocketWaitTimeoutMs = builder.getSocketWaitMs ();

		fLog = builder.getLog();
		if ( fLog == null ) throw new IllegalArgumentException ( "You must provide a logger." );

		fClock = builder.getClock ();

		// setup our HTTP client
		OkHttpClient.Builder okb = new OkHttpClient.Builder ()
			.connectTimeout ( 15, TimeUnit.SECONDS )
			.writeTimeout ( 15, TimeUnit.SECONDS )
			.readTimeout ( 30, TimeUnit.SECONDS )
		;

		// setup proxy
		final Proxy proxy = builder.getProxy ();
		if ( proxy != null )
		{
			okb = okb.proxy ( proxy );
		}
		fHttpClient = okb
			.build ()
		;

		fResponseParser = builder.fResponseParser;
		if ( fResponseParser == null )
		{
			throw new IllegalArgumentException ( "A response parser is required." );
		}

		fLabel = new StringBuilder ()
			.append ( fTopic )
			.append ( " on " )
			.append ( fHosts.toString () )
			.append ( " as " )
			.append ( fCreds.getUserDescription () )
			.toString ()
		;
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy