diff options
author | Taka Cho <takamune.cho@att.com> | 2021-02-19 09:00:43 -0500 |
---|---|---|
committer | Ajith Sreekumar <ajith.sreekumar@bell.ca> | 2021-02-19 17:41:43 +0000 |
commit | 2381590fc66aa088e0b016befaef09d21e618100 (patch) | |
tree | 6b307017031e92d0f6eb13b2461de1b212572364 /feature-server-pool/src/main | |
parent | 4d2730e9a66609f028dfa6289e035b1fe4924400 (diff) |
deprecate server-pool in droolspdp
deprecate server-pool in droolspdp
Issue-ID: POLICY-3079
Change-Id: Id25aea57cc4d119bba73de22c6dc77ab9a56c95f
Signed-off-by: Taka Cho <takamune.cho@att.com>
Diffstat (limited to 'feature-server-pool/src/main')
21 files changed, 0 insertions, 11725 deletions
diff --git a/feature-server-pool/src/main/feature/config/feature-server-pool.properties b/feature-server-pool/src/main/feature/config/feature-server-pool.properties deleted file mode 100644 index 00380294..00000000 --- a/feature-server-pool/src/main/feature/config/feature-server-pool.properties +++ /dev/null @@ -1,142 +0,0 @@ -### -# ============LICENSE_START======================================================= -# feature-server-pool -# ================================================================================ -# Copyright (C) 2020 AT&T Intellectual Property. All rights reserved. -# ================================================================================ -# 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. -# ============LICENSE_END========================================================= -### - -# The following properties control the IP address and port that a given -# server binds to. The default will bind to all interfaces on the host, -# and choose a port number at random. - -server.pool.server.ipAddress=${envd:SERVER_POOL_SERVER_IP} -server.pool.server.port=${envd:SERVER_POOL_PORT:20000} - -# The following properties determine whether HTTPS is used -- note that HTTPS -# also requires that the 'java.net.ssl.*' parameters in 'system.properties' be -# specified, and the key store and trust store be configured, as appropriate. -server.pool.server.https=${envd:SERVER_POOL_HTTPS} -server.pool.server.selfSignedCerts=false - -# The IP address and port that servers on the geo-redundant site -# should use to connect to servers on this site. -server.pool.server.site.ip=${envd:SERVER_POOL_SITE_IP} -server.pool.server.site.port=${envd:SERVER_POOL_SITE_PORT} - -# A comma-separated list of host names -- if an entry is found that refers -# to an HTTP/HTTPS destination IP address, the host name will used as the -# destination, instead of the IP address -server.pool.server.hostlist=${envd:SERVER_POOL_HOST_LIST} - -# The servers send 'pings' to each other once per main loop cycle. They -# also measure the gap between 'pings' from each server, and calculate -# an allowed time gap based upon this. 'server.pool.server.allowedGap' is the initial -# allowed gap prior to receiving any pings (default=30 seconds), and -# 'server.pool.server.adaptiveGapAdjust' is a value that is added to the calculated -# gap "just in case" (default=5 seconds) - -server.pool.server.allowedGap=30000 -server.pool.server.adaptiveGapAdjust=5000 - -# 'connectTimeout' and 'readTimeout' affect the client end of a REST -# connection (default=10 seconds each) - -server.pool.server.connectTimeout=10000 -server.pool.server.readTimeout=10000 - -# Each server has a thread pool per remote server, which is used when -# sending HTTP REST messages -- the following parameters determine the -# configuration. - -server.pool.server.threads.corePoolSize=5 -server.pool.server.threads.maximumPoolSize=10 -server.pool.server.threads.keepAliveTime=5000 - -# The server pool members use a UEB/DMAAP topic to connect with other -# servers in the pool. The following set of parameters are passed to -# the CambriaClient library, and are used in setting up the consumer and -# publisher ends of the connection. 'discovery.servers' and 'discovery.topic' -# are the minimum values that need to be specified. The last parameter in -# this set, 'discovery.publisherLoopCycleTime' isn't passed to the -# CambriaClient library; instead, it controls how frequently the 'ping' -# messages are sent out on this channel. Note that only the lead server -# keeps this channel open long-term. - -server.pool.discovery.servers=${envd:SERVER_POOL_DISCOVERY_SERVERS} -server.pool.discovery.port=${envd:SERVER_POOL_DISCOVERY_PORT:3904} -server.pool.discovery.topic=${envd:SERVER_POOL_DISCOVERY_TOPIC:DISCOVERY-TOPIC} -server.pool.discovery.username=${envd:SERVER_POOL_DISCOVERY_USERNAME} -server.pool.discovery.password=${envd:SERVER_POOL_DISCOVERY_PASSWORD} -server.pool.discovery.https=${envd:DMAAP_USE_HTTPS} -server.pool.discovery.apiKey= -server.pool.discovery.apiSecret= -#server.pool.discovery.publisherSocketTimeout=5000 -#server.pool.discovery.consumerSocketTimeout=70000 -server.pool.discovery.fetchTimeout=60000 -server.pool.discovery.fetchLimit=100 -server.pool.discovery.selfSignedCertificates=false -server.pool.discovery.publisherLoopCycleTime=5000 - -# The 'leader.*' parameters affect behavior during an election. The value of -# 'mainLoop.cycle' determines the actual time delay. 'leader.stableIdCycles' -# is the required minimum number of "stable" cycles before voting can start -# (in this case, "stable" means no servers added or failing). Once voting has -# started, "leader.stableVotingCycles' is the minimum number of "stable" -# cycles needed before declaring a winner (in this case, "stable" means no -# votes changing). - -server.pool.leader.stableIdleCycles=5 -server.pool.leader.stableVotingCycles=5 - -# The value of 'mainLoop.cycle' (default = 1 second) determines how frequently -# various events occur, such as the sending of 'ping' messages, and the -# duration of a "cycle" while voting for a lead server. - -server.pool.mainLoop.cycle=1000 - -# 'keyword.path' is used when looking for "keywords" within JSON messages. -# The first keyword located is hashed to determine which bucket to route -# through. - -keyword.path=requestID,CommonHeader.RequestID,body.output.common-header.request-id,result-info.request-id:uuid -# 'keyword.<CLASS-NAME>.lookup' is used to locate "keywords" within objects. -# The 'value' field contains a list of method calls or field names separated -# by '.' that are used to locate the keyword -# (e.g. 'method1().field2.method3()') - -keyword.java.lang.String.lookup=toString() -keyword.org.onap.policy.m2.base.Transaction.lookup=getRequestID() -keyword.org.onap.policy.controlloop.ControlLoopEvent.lookup=requestID -keyword.org.onap.policy.appclcm.LcmRequestWrapper.lookup=getBody().getCommonHeader().getRequestId() -keyword.org.onap.policy.appclcm.LcmResponseWrapper.lookup=getBody().getCommonHeader().getRequestId() -keyword.org.onap.policy.drools.serverpool.TargetLock.lookup=getOwnerKey() -keyword.org.onap.policy.drools.serverpooltest.TestDroolsObject.lookup=getKey() -keyword.org.onap.policy.drools.serverpooltest.Test1$KeywordWrapper.lookup=key - -# The following properties affect distributed 'TargetLock' behavior. -# -# server.pool.lock.ttl - controls how many hops a 'TargetLock' message can take -# server.pool.lock.audit.period - how frequently should the audit run? -# server.pool.lock.audit.gracePeriod - how long to wait after bucket reassignments -# before running the audit again -# server.pool.lock.audit.retryDelay - mismatches can occur due to the transient nature -# of the lock state: this property controls how long to wait before -# trying again - -server.pool.lock.ttl=3 -server.pool.lock.audit.period=300000 -server.pool.lock.audit.gracePeriod=60000 -server.pool.lock.audit.retryDelay=5000
\ No newline at end of file diff --git a/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/Bucket.java b/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/Bucket.java deleted file mode 100644 index a1afebc9..00000000 --- a/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/Bucket.java +++ /dev/null @@ -1,2579 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * feature-server-pool - * ================================================================================ - * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved. - * ================================================================================ - * 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. - * ============LICENSE_END========================================================= - */ - -package org.onap.policy.drools.serverpool; - -import static org.onap.policy.drools.serverpool.ServerPoolProperties.BUCKET_CONFIRMED_TIMEOUT; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.BUCKET_TIME_TO_LIVE; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.BUCKET_UNCONFIRMED_GRACE_PERIOD; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.BUCKET_UNCONFIRMED_TIMEOUT; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DEFAULT_BUCKET_CONFIRMED_TIMEOUT; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DEFAULT_BUCKET_TIME_TO_LIVE; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DEFAULT_BUCKET_UNCONFIRMED_GRACE_PERIOD; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DEFAULT_BUCKET_UNCONFIRMED_TIMEOUT; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.getProperty; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.io.PrintStream; -import java.io.Serializable; -import java.net.InetSocketAddress; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Base64; -import java.util.Comparator; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Queue; -import java.util.Random; -import java.util.Set; -import java.util.TreeMap; -import java.util.TreeSet; -import java.util.UUID; -import java.util.concurrent.Callable; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.FutureTask; -import java.util.concurrent.LinkedTransferQueue; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import javax.ws.rs.client.Entity; -import javax.ws.rs.client.WebTarget; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * The server pool uses an algorithmic way to map things like transactions - * (identified by a 'requestID') and locks (identified by a string key) - * into a server handling that transaction or lock. It does this by mapping - * the string name into one of a set of predefined hash buckets, with each - * bucket being assigned to one of the active servers. - * In other words: - * string key -> hash bucket (fixed mapping, known to all servers) - * hash bucket -> server (assignments may change when servers go up or down, - * but remains fairly static when the system is stable) - * With this approach, there is no global dynamic table that needs to be - * updated as transactions, or other objects come and go. - * Each instance of class 'Bucket' corresponds to one of the hash buckets, - * there are static methods that provide the overall abstraction, as well - * as some supporting classes. - */ - -@Getter -@Setter -public class Bucket { - private static Logger logger = LoggerFactory.getLogger(Bucket.class); - - /* - * Listener class to handle state changes that may lead to - * reassignments of buckets - */ - private static EventHandler eventHandler = new EventHandler(); - - // Used to hash keywords into buckets - private static MessageDigest messageDigest; - - static { - // register Listener class - Events.register(eventHandler); - - // create MD5 MessageDigest -- used to hash keywords - try { - // disabling sonar, as the digest is only used to hash keywords - it isn't - // used for security purposes - messageDigest = MessageDigest.getInstance("MD5"); // NOSONAR - } catch (NoSuchAlgorithmException e) { - throw new ExceptionInInitializerError(e); - } - } - - /* - * Values extracted from properties - */ - - private static String timeToLive; - private static long confirmedTimeout; - private static long unconfirmedTimeout; - private static long unconfirmedGracePeriod; - - /* - * Tags for encoding of bucket data - */ - private static final int END_OF_PARAMETERS_TAG = 0; - private static final int OWNER_UPDATE = 1; - private static final int OWNER_NULL = 2; - private static final int PRIMARY_BACKUP_UPDATE = 3; - private static final int PRIMARY_BACKUP_NULL = 4; - private static final int SECONDARY_BACKUP_UPDATE = 5; - private static final int SECONDARY_BACKUP_NULL = 6; - - // This is the table itself -- the current size is fixed at 1024 buckets - public static final int BUCKETCOUNT = 1024; - private static Bucket[] indexToBucket = new Bucket[BUCKETCOUNT]; - - static { - // create hash bucket entries, but there are no assignments yet - for (int i = 0; i < indexToBucket.length; i += 1) { - Bucket bucket = new Bucket(i); - indexToBucket[i] = bucket; - } - } - - // this is a list of all objects registered for the 'Backup' interface - private static List<Backup> backupList = new LinkedList<>(); - - // 'rebalance' is a non-null value when rebalancing is in progress - private static Object rebalanceLock = new Object(); - private static Rebalance rebalance = null; - - // bucket number - private volatile int index; - - // owner of the bucket -- this is the host where messages should be directed - private volatile Server owner = null; - - // this host will take over as the owner if the current owner goes down, - // and may also contain backup data to support persistence - private volatile Server primaryBackup = null; - - // this is a secondary backup host, which can be used if both owner and - // primary backup go out in quick succession - private volatile Server secondaryBackup = null; - - // when we are in a transient state, certain events are forwarded to - // this object - private volatile State state = null; - - // storage for additional data - private Map<Class<?>, Object> adjuncts = new HashMap<>(); - - // HTTP query parameters - private static final String QP_BUCKET = "bucket"; - private static final String QP_KEYWORD = "keyword"; - private static final String QP_DEST = "dest"; - private static final String QP_TTL = "ttl"; - private static final String OWNED_STR = "Owned"; - - // BACKUP data (only buckets for where we are the owner, or a backup) - - // TBD: need fields for outgoing queues for application message transfers - - /** - * This method triggers registration of 'eventHandler', and also extracts - * property values. - */ - static void startup() { - int intTimeToLive = - getProperty(BUCKET_TIME_TO_LIVE, DEFAULT_BUCKET_TIME_TO_LIVE); - timeToLive = String.valueOf(intTimeToLive); - confirmedTimeout = - getProperty(BUCKET_CONFIRMED_TIMEOUT, DEFAULT_BUCKET_CONFIRMED_TIMEOUT); - unconfirmedTimeout = - getProperty(BUCKET_UNCONFIRMED_TIMEOUT, - DEFAULT_BUCKET_UNCONFIRMED_TIMEOUT); - unconfirmedGracePeriod = - getProperty(BUCKET_UNCONFIRMED_GRACE_PERIOD, - DEFAULT_BUCKET_UNCONFIRMED_GRACE_PERIOD); - } - - /** - * Constructor -- called when building the 'indexToBucket' table. - * - * @param index the bucket number - */ - private Bucket(int index) { - this.index = index; - } - - /** - * This method converts a String keyword into the corresponding bucket - * number. - * - * @param value the keyword to be converted - * @return the bucket number - */ - public static int bucketNumber(String value) { - /* - * It would be possible to create a new 'MessageDigest' instance each - * It would be possible to create a new 'MessageDigest' instance each - * time this method is called, and avoid the need for synchronization. - * However, past experience has taught me that this might involve a - * considerable amount of computation, due to internal table - * initialization, so it shouldn't be done this way for performance - * reasons. - * If we start running into blocking issues because there are too many - * simultaneous calls to this method, we can initialize an array of these - * objects, and iterate over them using an AtomicInteger index. - */ - synchronized (messageDigest) { - /* - * Note that we only need the first two bytes of this, even though - * 16 bytes are produced. There may be other operations that can be - * used to more efficiently map keyword -> hash bucket. The only - * issue is the same algorithm must be used on all servers, and it - * should produce a fairly even distribution across all of the buckets. - */ - byte[] digest = messageDigest.digest(value.getBytes()); - return ((Byte.toUnsignedInt(digest[0]) << 8) - | Byte.toUnsignedInt(digest[1])) & 0x3ff; - } - } - - /** - * Fetch the server associated with a particular bucket number. - * - * @param bucketNumber a bucket number in the range 0-1023 - * @return the Server that currently handles the bucket, - * or 'null' if none is currently assigned - */ - public static Server bucketToServer(int bucketNumber) { - Bucket bucket = indexToBucket[bucketNumber]; - return bucket.getOwner(); - } - - /** - * Fetch the bucket object associated with a particular bucket number. - * - * @param bucketNumber a bucket number in the range 0-1023 - * @return the Bucket associated with this bucket number - */ - public static Bucket getBucket(int bucketNumber) { - return indexToBucket[bucketNumber]; - } - - /** - * Fetch the bucket object associated with a particular keyword. - * - * @param value the keyword to be converted - * @return the Bucket associated with this keyword - */ - public static Bucket getBucket(String value) { - return indexToBucket[bucketNumber(value)]; - } - - /** - * Determine if the associated key is assigned to the current server. - * - * @param key the keyword to be hashed - * @return 'true' if the associated bucket is assigned to this server, - * 'false' if not - */ - public static boolean isKeyOnThisServer(String key) { - int bucketNumber = bucketNumber(key); - Bucket bucket = indexToBucket[bucketNumber]; - return bucket.getOwner() == Server.getThisServer(); - } - - /** - * Handle an incoming /bucket/update REST message. - * - * @param data base64-encoded data, containing all bucket updates - */ - static void updateBucket(byte[] data) { - final byte[] packet = Base64.getDecoder().decode(data); - Runnable task = () -> { - try { - /* - * process the packet, handling any updates - */ - if (updateBucketInternal(packet)) { - /* - * updates have occurred -- forward this packet to - * all servers in the "notify list" - */ - logger.info("One or more bucket updates occurred"); - Entity<String> entity = - Entity.entity(new String(data, StandardCharsets.UTF_8), - MediaType.APPLICATION_OCTET_STREAM_TYPE); - for (Server server : Server.getNotifyList()) { - server.post("bucket/update", entity); - } - } - } catch (Exception e) { - logger.error("Exception in Bucket.updateBucket", e); - } - }; - MainLoop.queueWork(task); - } - - /** - * This method supports the 'updateBucket' method, and runs entirely within - * the 'MainLoop' thread. - */ - private static boolean updateBucketInternal(byte[] packet) throws IOException { - boolean changes = false; - - ByteArrayInputStream bis = new ByteArrayInputStream(packet); - DataInputStream dis = new DataInputStream(bis); - - // the packet contains a sequence of bucket updates - while (dis.available() != 0) { - // first parameter = bucket number - int index = dis.readUnsignedShort(); - - // locate the corresponding 'Bucket' object - Bucket bucket = indexToBucket[index]; - - // indicates whether changes occurred to the bucket - boolean bucketChanges = false; - - /* - * the remainder of the information for this bucket consists of - * a sequence of '<tag> [ <associated-data> ]' followed by the tag - * value 'END_OF_PARAMETERS_TAG'. - */ - int tag; - while ((tag = dis.readUnsignedByte()) != END_OF_PARAMETERS_TAG) { - switch (tag) { - case OWNER_UPDATE: - // <OWNER_UPDATE> <owner-uuid> -- owner UUID specified - bucketChanges = updateBucketInternalOwnerUpdate(bucket, dis, index); - break; - - case OWNER_NULL: - // <OWNER_NULL> -- owner UUID should be set to 'null' - bucketChanges = nullifyOwner(index, bucket, bucketChanges); - break; - - case PRIMARY_BACKUP_UPDATE: - // <PRIMARY_BACKUP_UPDATE> <primary-backup-uuid> -- - // primary backup UUID specified - bucketChanges = updatePrimaryBackup(dis, index, bucket, bucketChanges); - break; - - case PRIMARY_BACKUP_NULL: - // <PRIMARY_BACKUP_NULL> -- - // primary backup should be set to 'null' - bucketChanges = nullifyPrimaryBackup(index, bucket, bucketChanges); - break; - - case SECONDARY_BACKUP_UPDATE: - // <SECONDARY_BACKUP_UPDATE> <secondary-backup-uuid> -- - // secondary backup UUID specified - bucketChanges = updateSecondaryBackup(dis, index, bucket, bucketChanges); - break; - - case SECONDARY_BACKUP_NULL: - // <SECONDARY_BACKUP_NULL> -- - // secondary backup should be set to 'null' - bucketChanges = nullifySecondaryBackup(index, bucket, bucketChanges); - break; - - default: - logger.error("Illegal tag: {}", tag); - break; - } - } - if (bucketChanges) { - // give audit a chance to run - changes = true; - bucket.stateChanged(); - } - } - return changes; - } - - private static boolean nullifyOwner(int index, Bucket bucket, boolean bucketChanges) { - if (bucket.getOwner() != null) { - logger.info("Bucket {} owner: {}->null", - index, bucket.getOwner()); - bucketChanges = true; - bucket.nullifyOwner(); - } - return bucketChanges; - } - - private synchronized void nullifyOwner() { - setOwner(null); - setState(null); - } - - /** - * Gets the set of backups. - * - * @return the set of backups - */ - public synchronized Set<Server> getBackups() { - /* - * For some reason, the junit tests break if Set.of() is used, so we'll stick with - * the long way for now. - */ - Set<Server> backups = new HashSet<>(); - backups.add(getPrimaryBackup()); - backups.add(getSecondaryBackup()); - return backups; - } - - private static boolean updatePrimaryBackup(DataInputStream dis, int index, Bucket bucket, boolean bucketChanges) - throws IOException { - Server newPrimaryBackup = - Server.getServer(Util.readUuid(dis)); - if (bucket.primaryBackup != newPrimaryBackup) { - logger.info("Bucket {} primary backup: {}->{}", index, - bucket.primaryBackup, newPrimaryBackup); - bucketChanges = true; - bucket.primaryBackup = newPrimaryBackup; - } - return bucketChanges; - } - - private static boolean nullifyPrimaryBackup(int index, Bucket bucket, boolean bucketChanges) { - if (bucket.primaryBackup != null) { - logger.info("Bucket {} primary backup: {}->null", - index, bucket.primaryBackup); - bucketChanges = true; - bucket.primaryBackup = null; - } - return bucketChanges; - } - - private static boolean updateSecondaryBackup(DataInputStream dis, int index, Bucket bucket, boolean bucketChanges) - throws IOException { - Server newSecondaryBackup = - Server.getServer(Util.readUuid(dis)); - if (bucket.secondaryBackup != newSecondaryBackup) { - logger.info("Bucket {} secondary backup: {}->{}", index, - bucket.secondaryBackup, newSecondaryBackup); - bucketChanges = true; - bucket.secondaryBackup = newSecondaryBackup; - } - return bucketChanges; - } - - private static boolean nullifySecondaryBackup(int index, Bucket bucket, boolean bucketChanges) { - if (bucket.secondaryBackup != null) { - logger.info("Bucket {} secondary backup: {}->null", - index, bucket.secondaryBackup); - bucketChanges = true; - bucket.secondaryBackup = null; - } - return bucketChanges; - } - - /** - * Update bucket owner information. - * - * @param bucket the bucket in process - * @param dis data input stream contains the update - * @param index the bucket number - * @return a value indicate bucket changes - */ - private static boolean updateBucketInternalOwnerUpdate(Bucket bucket, DataInputStream dis, - int index) throws IOException { - boolean bucketChanges = false; - Server newOwner = Server.getServer(Util.readUuid(dis)); - if (bucket.getOwner() != newOwner) { - logger.info("Bucket {} owner: {}->{}", - index, bucket.getOwner(), newOwner); - bucketChanges = true; - - Server thisServer = Server.getThisServer(); - Server oldOwner = bucket.getOwner(); - bucket.setOwner(newOwner); - if (thisServer == oldOwner) { - // the current server is the old owner - if (bucket.getState() == null) { - bucket.state = bucket.new OldOwner(newOwner); - } - } else if (thisServer == newOwner) { - // the current server the new owner - if (bucket.getState() == null) { - bucket.state = bucket.new NewOwner(true, oldOwner); - } else { - // new owner has been confirmed - bucket.state.newOwner(); - } - } - } - return bucketChanges; - } - - /** - * Forward a message to the specified bucket number. If the bucket is - * in a transient state (the value of 'state' is not 'null'), the handling - * is determined by that state. - * - * @param bucketNumber the bucket number determined by extracting the - * keyword from 'message' - * @param message the message to be forwarded/processed - * @return a value of 'true' indicates the message has been "handled" - * (forwarded or queued), and 'false' indicates it has not, and needs - * to be handled locally. - */ - public static boolean forward(int bucketNumber, Message message) { - Bucket bucket = indexToBucket[bucketNumber]; - Server server; - - synchronized (bucket) { - if (bucket.state != null) { - // we are in a transient state -- the handling is state-specific - return bucket.state.forward(message); - } - server = bucket.getOwner(); - } - - if (server == null || server == Server.getThisServer()) { - // this needs to be processed locally - return false; - } else { - // send message to remote server - message.sendToServer(server, bucketNumber); - return true; - } - } - - /** - * This is a convenience method, which forwards a message through the - * bucket associated with the specified keyword. - * - * @param keyword the keyword extracted from 'message' - * keyword from 'message' - * @param message the message to be forwarded/processed - * @return a value of 'true' indicates the message has been "handled" - * (forwarded or queued), and 'false' indicates it has not, and needs - * to be handled locally. - */ - public static boolean forward(String keyword, Message message) { - return forward(bucketNumber(keyword), message); - } - - /** - * Forward a message to the specified bucket number. If the bucket is - * in a transient state (the value of 'state' is not 'null'), the handling - * is determined by that state. This is a variant of the 'forward' method, - * which handles local processing, instead of just returning 'false'. - * - * @param bucketNumber the bucket number determined by extracting the - * keyword from 'message' - * @param message the message to be forwarded/processed - */ - public static void forwardAndProcess(int bucketNumber, Message message) { - if (!forward(bucketNumber, message)) { - message.process(); - } - } - - /** - * Forward a message to the specified bucket number. If the bucket is - * in a transient state (the value of 'state' is not 'null'), the handling - * is determined by that state. This is a variant of the 'forward' method, - * which handles local processing, instead of just returning 'false'. - * - * @param keyword the keyword extracted from 'message' - * keyword from 'message' - * @param message the message to be forwarded/processed - */ - public static void forwardAndProcess(String keyword, Message message) { - forwardAndProcess(bucketNumber(keyword), message); - } - - /** - * Handle an incoming /cmd/dumpBuckets REST message. - * - * @param out the 'PrintStream' to use for displaying information - */ - public static void dumpBuckets(final PrintStream out) { - /* - * we aren't really doing a 'Rebalance' here, but the 'copyData' method - * is useful for extracting the data, and determining the buckets - * associated with each server. - */ - Rebalance rb = new Rebalance(); - rb.copyData(); - - /* - * this method is not accessing anything in the 'Server' or 'Bucket' - * table, so it doesn't need to run within the 'MainLoop' thread. - */ - rb.dumpBucketsInternal(out); - } - - /** - * Handle an incoming /cmd/bucketMessage REST message -- this is only - * used for testing the routing of messages between servers. - * - * @param out the 'PrintStream' to use for displaying information - * @param keyword the keyword that is hashed to select the bucket number - * @param message the message to send to the remote end - * @throws IOException when error occurred - */ - public static void bucketMessage( - final PrintStream out, final String keyword, String message) { - - if (keyword == null) { - out.println("'keyword' is mandatory"); - return; - } - if (message == null) { - message = "Message generated at " + new Date(); - } - final int bucketNumber = bucketNumber(keyword); - Server server = bucketToServer(bucketNumber); - - if (server == null) { - /* - * selected bucket has no server assigned -- this should only be a - * transient situation, until 'rebalance' is run. - */ - out.format("Bucket is %d, which has no owner%n", bucketNumber); - } else if (server == Server.getThisServer()) { - /* - * the selected bucket is associated with this particular server -- - * no forwarding is needed. - */ - out.format("Bucket is %d, which is owned by this server: %s%n", - bucketNumber, server.getUuid()); - } else { - /* - * the selected bucket is assigned to a different server -- forward - * the message. - */ - out.format("Bucket is %d: sending from%n" - + " %s to%n" - + " %s%n", - bucketNumber, Server.getThisServer().getUuid(), server.getUuid()); - - // do a POST call of /bucket/bucketResponse to the remoote server - Entity<String> entity = - Entity.entity(new String(message.getBytes(), StandardCharsets.UTF_8), - MediaType.TEXT_PLAIN); - - /* - * the POST itself runs in a server-specific thread, and - * 'responseQueue' is used to pass back the response. - */ - final LinkedTransferQueue<Response> responseQueue = - new LinkedTransferQueue<>(); - - server.post("bucket/bucketResponse", entity, new Server.PostResponse() { - /** - * {@inheritDoc} - */ - @Override - public WebTarget webTarget(WebTarget webTarget) { - // we need to include the 'bucket' and 'keyword' parameters - // in the POST that we are sending out - return webTarget - .queryParam(QP_BUCKET, bucketNumber) - .queryParam(QP_KEYWORD, keyword); - } - - /** - * {@inheritDoc} - */ - @Override - public void response(Response response) { - // this is the POST response -- - // pass it back to the calling thread - responseQueue.put(response); - } - }); - - try { - // this is the calling thread -- wait for the POST response - Response response = responseQueue.poll(60, TimeUnit.SECONDS); - if (response == null) { - out.println("Timed out waiting for a response"); - } else { - out.format("Received response code %s%nEntity = %s%n", - response.getStatus(), response.readEntity(String.class)); - } - } catch (InterruptedException e) { - out.println(e); - Thread.currentThread().interrupt(); - } - } - } - - /** - * Handle an incoming /bucket/bucketResponse REST message -- this runs on - * the destination host, and is the continuation of an operation triggered - * by the /cmd/bucketMessage REST message running on the originating host. - * - * @param out the 'PrintStream' to use for passing back information - * in a human-readable form - * @param bucket the bucket number, which should be owned by this host - * if we are in sync with the sending host, and didn't get caught - * in a transient state - * @param keyword the keyword selected on the originating end, which should - * hash to 'bucket' - * @param message the message selected on the originating end - */ - public static void bucketResponse( - final PrintStream out, int bucket, String keyword, byte[] message) { - - Server thisServer = Server.getThisServer(); - Server server = bucketToServer(bucket); - - if (server != thisServer) { - /* - * this isn't expected, and either indicates we are out-of-sync with - * pthe originating server, or this operation was triggered while in - * a transient state. - */ - out.println("ERROR: " + thisServer.toString() + ": bucket " + bucket - + "is owned by\n " + server); - } else { - /* - * As expected, we are the owner of this bucket. Print out a message, - * which will be returned to the originating host, and displayed. - */ - out.println(thisServer.toString() + ":\n" - + " bucket = " + bucket - + "\n keyword = " + keyword - + "\n message = " + new String(message)); - } - } - - /** - * Handle an incoming /cmd/moveBucket REST message -- this is only - * used for testing bucket migration. It only works on the lead server. - * - * @param out the 'PrintStream' to use for displaying information - * @param bucketNumber the bucket number to be moved - * @param newHostUuid the UUID of the destination host (if 'null', a - * destination host will be chosen at random) - */ - public static void moveBucket(PrintStream out, int bucketNumber, String newHostUuid) { - Server leader = Leader.getLeader(); - if (leader != Server.getThisServer()) { - out.println("This is not the lead server"); - return; - } - - if (bucketNumber < 0 || bucketNumber >= indexToBucket.length) { - out.println("Bucket number out of range"); - return; - } - - Rebalance rb = new Rebalance(); - rb.copyData(); - - TestBucket bucket = rb.buckets[bucketNumber]; - TestServer oldHost = bucket.owner; - - if (oldHost == rb.nullServer) { - out.println("Bucket " + bucketNumber + " is currently unassigned"); - return; - } - - TestServer newHost = null; - - if (newHostUuid != null) { - // the UUID of a destination host has been specified - newHost = rb.testServers.get(UUID.fromString(newHostUuid)); - if (newHost == null) { - out.println("Can't locate UUID " + newHostUuid); - return; - } - } else { - /* - * Choose a destination host at random, other than the current owner. - * Step a random count in the range of 1 to (n-1) relative to the - * current host. - */ - UUID key = oldHost.uuid; - /* - * Disabling sonar, because this Random() is not used for security purposes. - */ - int randomStart = new Random().nextInt(rb.testServers.size() - 1); // NOSONAR - for (int count = randomStart; count >= 0; count -= 1) { - key = rb.testServers.higherKey(key); - if (key == null) { - // wrap to the beginning of the list - key = rb.testServers.firstKey(); - } - } - newHost = rb.testServers.get(key); - } - out.println("Moving bucket " + bucketNumber + " from " - + oldHost + " to " + newHost); - - updateOwner(out, bucket, oldHost, newHost); - - try { - /* - * build a message containing all of the updated bucket - * information, process it internally in this host - * (lead server), and send it out to others in the - * "notify list". - */ - rb.generateBucketMessage(); - } catch (IOException e) { - logger.error("Exception in Rebalance.generateBucketMessage", - e); - } - } - - private static void updateOwner(PrintStream out, TestBucket bucket, TestServer oldHost, TestServer newHost) { - /* - * update the owner, and ensure that the primary and secondary backup - * remain different from the owner. - */ - bucket.setOwner(newHost); - if (newHost == bucket.primaryBackup) { - out.println("Moving primary back from " + newHost + " to " + oldHost); - bucket.setPrimaryBackup(oldHost); - } else if (newHost == bucket.secondaryBackup) { - out.println("Moving secondary back from " + newHost - + " to " + oldHost); - bucket.setSecondaryBackup(oldHost); - } - } - - /** - * This method is called when an incoming /bucket/sessionData message is - * received from the old owner of the bucket, which presumably means that - * we are the new owner of the bucket. - * - * @param bucketNumber the bucket number - * @param dest the UUID of the intended destination - * @param ttl similar to IP time-to-live -- it controls the number of hops - * the message may take - * @param data serialized data associated with this bucket, encoded using - * base64 - */ - - static void sessionData(int bucketNumber, UUID dest, int ttl, byte[] data) { - logger.info("Bucket.sessionData: bucket={}, data length={}", - bucketNumber, data.length); - - if (dest != null && !dest.equals(Server.getThisServer().getUuid())) { - // the message needs to be forwarded to the intended destination - Server server; - WebTarget webTarget; - - ttl -= 1; - if (ttl > 0 - && (server = Server.getServer(dest)) != null - && (webTarget = server.getWebTarget("bucket/sessionData")) != null) { - logger.info("Forwarding 'bucket/sessionData' to uuid {}", - server.getUuid()); - Entity<String> entity = - Entity.entity(new String(data, StandardCharsets.UTF_8), - MediaType.APPLICATION_OCTET_STREAM_TYPE); - Response response = - webTarget - .queryParam(QP_BUCKET, bucketNumber) - .queryParam(QP_DEST, dest) - .queryParam(QP_TTL, String.valueOf(ttl)) - .request().post(entity); - logger.info("/bucket/sessionData response code = {}", - response.getStatus()); - } else { - logger.error("Couldn't forward 'bucket/sessionData' to uuid {}, ttl={}", - dest, ttl); - } - return; - } - - byte[] decodedData = Base64.getDecoder().decode(data); - Bucket bucket = indexToBucket[bucketNumber]; - - logger.info("Bucket.sessionData: decoded data length = {}", - decodedData.length); - - if (bucket.state == null) { - /* - * We received the serialized data prior to being notified - * that we are the owner -- this happens sometimes. Behave as - * though we are the new owner, but intidate it is unconfirmed. - */ - logger.info("Bucket {} session data received unexpectedly", - bucketNumber); - bucket.state = bucket.new NewOwner(false, bucket.getOwner()); - } - bucket.state.bulkSerializedData(decodedData); - } - - /** - * This method is called whenever the bucket's state has changed in a - * way that it should be audited. - */ - private synchronized void stateChanged() { - if (state != null) { - return; - } - // the audit should be run - Server thisServer = Server.getThisServer(); - boolean isOwner = (thisServer == owner); - boolean isBackup = (!isOwner && (thisServer == primaryBackup - || thisServer == secondaryBackup)); - - // invoke 'TargetLock' directly - TargetLock.auditBucket(this, isOwner); - for (ServerPoolApi feature : ServerPoolApi.impl.getList()) { - feature.auditBucket(this, isOwner, isBackup); - } - } - - /** - * Returns an adjunct of the specified class - * (it is created if it doesn't exist). - * - * @param clazz this is the class of the adjunct - * @return an adjunct of the specified class ('null' may be returned if - * the 'newInstance' method is unable to create the adjunct) - */ - public <T> T getAdjunct(Class<T> clazz) { - Object adj = adjuncts.computeIfAbsent(clazz, key -> { - try { - // create the adjunct, if needed - return clazz.getDeclaredConstructor().newInstance(); - } catch (Exception e) { - logger.error("Can't create adjunct of {}", clazz, e); - return null; - } - }); - return clazz.cast(adj); - } - - /** - * Returns an adjunct of the specified class. - * - * @param clazz this is the class of the adjunct - * @return an adjunct of the specified class, if it exists, - * and 'null' if it does not - */ - public <T> T getAdjunctDontCreate(Class<T> clazz) { - synchronized (adjuncts) { - // look up the adjunct in the table - return clazz.cast(adjuncts.get(clazz)); - } - } - - /** - * Explicitly create an adjunct -- this is useful when the adjunct - * initialization requires that some parameters be passed. - * - * @param adj this is the adjunct to insert into the table - * @return the previous adjunct of this type ('null' if none) - */ - public Object putAdjunct(Object adj) { - synchronized (adjuncts) { - Class<?> clazz = adj.getClass(); - return adjuncts.put(clazz, adj); - } - } - - /** - * Remove an adjunct. - * - * @param clazz this is the class of adjuncts to remove - * @return the object, if found, and 'null' if not - */ - public <T> T removeAdjunct(Class<T> clazz) { - synchronized (adjuncts) { - // remove the adjunct in the table - return clazz.cast(adjuncts.remove(clazz)); - } - } - - /** - * Dump out all buckets with adjuncts. - * - * @param out the 'PrintStream' to use for displaying information - */ - public static void dumpAdjuncts(PrintStream out) { - boolean noneFound = true; - String format = "%6s %s\n"; - - for (Bucket bucket : indexToBucket) { - synchronized (bucket.adjuncts) { - if (bucket.adjuncts.size() != 0) { - if (noneFound) { - out.printf(format, "Bucket", "Adjunct Classes"); - out.printf(format, "------", "---------------"); - noneFound = false; - } - boolean first = true; - for (Class<?> clazz : bucket.adjuncts.keySet()) { - if (first) { - out.printf(format, bucket.index, clazz.getName()); - first = false; - } else { - out.printf(format, "", clazz.getName()); - } - } - } - } - } - } - - /* ============================================================ */ - - /** - * There is a single instance of this class (Bucket.eventHandler), which - * is registered to listen for notifications of state transitions. Note - * that all of these methods are running within the 'MainLoop' thread. - */ - private static class EventHandler extends Events { - /** - * {@inheritDoc} - */ - @Override - public void serverFailed(Server server) { - // remove this server from any bucket where it is referenced - - Server thisServer = Server.getThisServer(); - for (Bucket bucket : indexToBucket) { - synchronized (bucket) { - boolean changes = false; - if (bucket.getOwner() == server) { - // the failed server owns this bucket -- - // move to the primary backup - bucket.setOwner(bucket.getPrimaryBackup()); - bucket.primaryBackup = null; - changes = true; - - if (bucket.getOwner() == null) { - // bucket owner is still null -- presumably, we had no - // primary backup, so use the secondary backup instead - bucket.setOwner(bucket.getSecondaryBackup()); - bucket.setSecondaryBackup(null); - } - } - if (bucket.getPrimaryBackup() == server) { - // the failed server was a primary backup to this bucket -- - // mark the entry as 'null' - bucket.setPrimaryBackup(null); - changes = true; - } - if (bucket.getSecondaryBackup() == server) { - // the failed server was a secondary backup to this bucket -- - // mark the entry as 'null' - bucket.setSecondaryBackup(null); - changes = true; - } - - if (bucket.owner == thisServer && bucket.state == null) { - // the current server is the new owner - bucket.setState(bucket.new NewOwner(false, null)); - changes = true; - } - - if (changes) { - // may give audits a chance to run - bucket.stateChanged(); - } - } - } - - // trigger a rebalance (only happens if we are the lead server) - rebalance(); - } - - /** - * {@inheritDoc} - */ - @Override - public void newLeader(Server server) { - // trigger a rebalance (only happens if we are the new lead server) - rebalance(); - } - - /** - * {@inheritDoc} - */ - @Override - public void leaderConfirmed(Server server) { - // trigger a rebalance (only happens if we are the lead server) - rebalance(); - } - - /** - * This method is called to start a 'rebalance' operation in a background - * thread, but it only does this on the lead server. Being balanced means - * the following: - * 1) Each server owns approximately the same number of buckets - * 2) If any server were to fail, and the designated primaries take over - * for all of that server's buckets, all remaining servers would still - * own approximately the same number of buckets. - * 3) If any two servers were to fail, and the designated primaries were - * to take over for the failed server's buckets (secondaries would take - * for buckets where the owner and primary are OOS), all remaining - * servers would still own approximately the same number of buckets. - * 4) Each server should have approximately the same number of - * (primary-backup + secondary-backup) buckets that it is responsible for. - * 5) The primary backup for each bucket must be on the same site as the - * owner, and the secondary backup must be on a different site. - */ - private void rebalance() { - if (Leader.getLeader() == Server.getThisServer()) { - Rebalance rb = new Rebalance(); - synchronized (rebalanceLock) { - // the most recent 'Rebalance' instance is the only valid one - rebalance = rb; - } - - new Thread("BUCKET REBALANCER") { - @Override - public void run() { - /* - * copy bucket and host data, - * generating a temporary internal table. - */ - rb.copyData(); - - /* - * allocate owners for all buckets without an owner, - * and rebalance bucket owners, if necessary -- - * this takes card of item #1, above. - */ - rb.allocateBuckets(); - - /* - * make sure that primary backups always have the same site - * as the owner, and secondary backups always have a different - * site -- this takes care of #5, above. - */ - rb.checkSiteValues(); - - /* - * adjust primary backup lists to take care of item #2, above - * (taking #5 into account). - */ - rb.rebalancePrimaryBackups(); - - /* - * allocate secondary backups, and take care of items - * #3 and #4, above (taking #5 into account). - */ - rb.rebalanceSecondaryBackups(); - - try { - synchronized (rebalanceLock) { - /* - * if another 'Rebalance' instance has started in the - * mean time, don't do the update. - */ - if (rebalance == rb) { - /* - * build a message containing all of the updated bucket - * information, process it internally in this host - * (lead server), and send it out to others in the - * "notify list". - */ - rb.generateBucketMessage(); - rebalance = null; - } - } - } catch (IOException e) { - logger.error("Exception in Rebalance.generateBucketMessage", - e); - } - } - }.start(); - } - } - } - - /* ============================================================ */ - - /** - * Instances of this class are created as part of the 'rebalance' - * operation on the lead server, or as part of a 'dumpBuckets' operation - * on any server. - * Each instance of this class corresponds to a 'Bucket' instance. - */ - @EqualsAndHashCode - private static class TestBucket implements Comparable<TestBucket> { - // bucket number - int index; - - // owner of the bucket - TestServer owner; - - // primary backup for this bucket - - TestServer primaryBackup; - - // secondary backup for this bucket - TestServer secondaryBackup; - - /** - * Constructor -- initialize the 'TestBucket' instance. - * - * @param index the bucket number - */ - TestBucket(int index) { - this.index = index; - } - - /** - * Update the owner of a bucket, which also involves updating the - * backward links in the 'TestServer' instances. - * - * @param newOwner the new owner of the bucket - */ - void setOwner(TestServer newOwner) { - if (owner != newOwner) { - // the 'owner' field does need to be changed - if (owner != null) { - // remove this bucket from the 'buckets' list of the old owner - owner.buckets.remove(this); - } - if (newOwner != null) { - // add this bucket to the 'buckets' list of the new owner - newOwner.buckets.add(this); - } - // update the 'owner' field in the bucket - owner = newOwner; - } - } - - /** - * Update the primary backup of a bucket, which also involves updating - * the backward links in the 'TestServer' instances. - * - * @param newPrimaryBackup the new primary of the bucket - */ - void setPrimaryBackup(TestServer newPrimaryBackup) { - if (primaryBackup != newPrimaryBackup) { - // the 'primaryBackup' field does need to be changed - if (primaryBackup != null) { - // remove this bucket from the 'buckets' list of the - // old primary backup - primaryBackup.primaryBackupBuckets.remove(this); - } - if (newPrimaryBackup != null) { - // add this bucket to the 'buckets' list of the - // new primary backup - newPrimaryBackup.primaryBackupBuckets.add(this); - } - // update the 'primaryBackup' field in the bucket - primaryBackup = newPrimaryBackup; - } - } - - /** - * Update the secondary backup of a bucket, which also involves updating - * the backward links in the 'TestServer' instances. - * - * @param newSecondaryBackup the new secondary of the bucket - */ - void setSecondaryBackup(TestServer newSecondaryBackup) { - if (secondaryBackup != newSecondaryBackup) { - // the 'secondaryBackup' field does need to be changed - if (secondaryBackup != null) { - // remove this bucket from the 'buckets' list of the - // old secondary backup - secondaryBackup.secondaryBackupBuckets.remove(this); - } - if (newSecondaryBackup != null) { - // add this bucket to the 'buckets' list of the - // new secondary backup - newSecondaryBackup.secondaryBackupBuckets.add(this); - } - // update the 'secondaryBackup' field in the bucket - secondaryBackup = newSecondaryBackup; - } - } - - /*==================================*/ - /* Comparable<TestBucket> interface */ - /*==================================*/ - - /** - * Compare two 'TestBucket' instances, for use in a 'TreeSet'. - * - * @param other the other 'TestBucket' instance to compare to - */ - @Override - public int compareTo(TestBucket other) { - return index - other.index; - } - - /** - * Return a string representation of this 'TestBucket' instance. - * - * @return a string representation of this 'TestBucket' instance - */ - @Override - public String toString() { - return "TestBucket[" + index + "]"; - } - } - - /* ============================================================ */ - - /** - * Instances of this class are created as part of the 'rebalance' - * operation on the lead server, or as part of a 'dumpBuckets' operation - * on any server. - * Each instance of this class corresponds to a 'Server' instance. - * Unlike the actual 'Server' instances, each 'TestServer' instance - * contains back links to all of the buckets it is associated with. - */ - private static class TestServer { - // unique id for this server - // (matches the one in the corresponding 'Server' entry) - final UUID uuid; - - // site socket information (matches 'Server' entry) - final InetSocketAddress siteSocketAddress; - - // the set of all 'TestBucket' instances, - // where this 'TestServer' is listed as 'owner' - final TreeSet<TestBucket> buckets = new TreeSet<>(); - - // the set of all 'TestBucket' instances, - // where this 'TestServer' is listed as 'primaryBackup' - final TreeSet<TestBucket> primaryBackupBuckets = new TreeSet<>(); - - // the set of all 'TestBucket' instances, - // where this 'TestServer' is listed as 'secondaryBackup' - final TreeSet<TestBucket> secondaryBackupBuckets = new TreeSet<>(); - - /** - * Constructor. - * - * @param uuid uuid of this 'TestServer' instance - * @param siteSocketAddress matches the value in the corresponding 'Server' - */ - TestServer(UUID uuid, InetSocketAddress siteSocketAddress) { - this.uuid = uuid; - this.siteSocketAddress = siteSocketAddress; - } - - /** - * Return a string representation of this 'TestServer' instance. - * - * @return a string representation of this 'TestServer' instance - */ - @Override - public String toString() { - return "TestServer[" + uuid + "]"; - } - } - - /* ============================================================ */ - - /** - * This class supports the 'rebalance' operation. Each instance is a wrapper - * around a 'TestServer' instance, as it would be if another specific - * server failed. - */ - @EqualsAndHashCode - private static class AdjustedTestServer - implements Comparable<AdjustedTestServer> { - TestServer server; - - // simulated fail on this server - TestServer failedServer; - - // expected bucket count if 'failedServer' fails - int bucketCount; - - // total number of primary backup buckets used by this host - int primaryBackupBucketCount; - - // total number of secondary backup buckets used by this host - int secondaryBackupBucketCount; - - /** - * Constructor. - * - * @param server the server this 'AdjustedTestServer' instance represents - * @param failedServer the server going through a simulated failure -- - * the 'bucketCount' value is adjusted based upon this - */ - AdjustedTestServer(TestServer server, TestServer failedServer) { - this.server = server; - this.failedServer = failedServer; - - this.bucketCount = server.buckets.size(); - this.primaryBackupBucketCount = server.primaryBackupBuckets.size(); - this.secondaryBackupBucketCount = server.secondaryBackupBuckets.size(); - - // need to adjust 'bucketCount' for the case where the current - // host fails - for (TestBucket bucket : server.primaryBackupBuckets) { - if (bucket.owner == failedServer) { - bucketCount += 1; - // TBD: should 'primaryBackupBucketCount' be decremented? - } - } - - // need to adjust 'bucketCount' for the case where the current - // host fails - for (TestBucket bucket : server.secondaryBackupBuckets) { - if (bucket.owner == failedServer) { - bucketCount += 1; - // TBD: should 'secondaryBackupBucketCount' be decremented? - } - } - } - - /* ****************************************** */ - /* Comparable<AdjustedTestServer> interface */ - /* ****************************************** */ - - /** - * {@inheritDoc} - */ - @Override - public int compareTo(AdjustedTestServer other) { - /* - * Comparison order: - * 1) minimal expected bucket count if current host fails - * (differences of 1 are treated as a match) - * 2) minimal backup bucket count - * 3) UUID order - */ - int rval = bucketCount - other.bucketCount; - if (rval <= 1 && rval >= -1) { - rval = (primaryBackupBucketCount + secondaryBackupBucketCount) - - (other.primaryBackupBucketCount - + other.secondaryBackupBucketCount); - if (rval == 0) { - rval = -Util.uuidComparator.compare(server.uuid, other.server.uuid); - } - } - return rval; - } - } - - /* ============================================================ */ - - /** - * This class is primarily used to do a 'Rebalance' operation on the - * lead server, which is then distributed to all of the other servers. - * Part of it is also useful for implementing the /cmd/dumpBuckets - * REST message handler. - */ - private static class Rebalance { - // this table resembles the 'Bucket.indexToBucket' table - TestBucket[] buckets = new TestBucket[indexToBucket.length]; - - // this table resembles the 'Server.servers' table - TreeMap<UUID, TestServer> testServers = new TreeMap<>(Util.uuidComparator); - - /* this is a special server instance, which is allocated any - * owned, primary, or secondary buckets that haven't been allocated to - * any of the real servers - */ - TestServer nullServer = new TestServer(null, null); - - /** - * Copy all of the bucket data in the 'buckets' table, and also return - * a copy of the 'Server.servers' table - */ - void copyData() { - // will contain a copy of the 'Bucket' table - final Bucket[] bucketSnapshot = new Bucket[indexToBucket.length]; - - /* - * This method is running within the 'MainLoop' thread, - * and builds a copy of the 'Bucket' table, as well as the - * list of active servers -- these can then be examined - * in a different thread, without potentially distrupting - * the 'MainLoop' thread. - * - * @return 0 (the return value is not significant at present) - */ - Callable<Integer> callable = () -> { - // copy the 'Bucket' table - for (int i = 0; i < indexToBucket.length; i += 1) { - // makes a snapshot of the bucket information - Bucket bucket = indexToBucket[i]; - - Bucket tmpBucket = new Bucket(i); - tmpBucket.setOwner(bucket.getOwner()); - tmpBucket.setPrimaryBackup(bucket.getPrimaryBackup()); - tmpBucket.setSecondaryBackup(bucket.getSecondaryBackup()); - bucketSnapshot[i] = tmpBucket; - } - - /* - * At this point, 'bucketSnapshot' and 'servers' should be - * complete. The next step is to create a 'TestServer' entry - * that matches each 'Server' entry. - */ - for (Server server : Server.getServers()) { - UUID uuid = server.getUuid(); - testServers.put(uuid, new TestServer(uuid, server.getSiteSocketAddress())); - } - - return 0; - }; - FutureTask<Integer> ft = new FutureTask<>(callable); - MainLoop.queueWork(ft); - try { - ft.get(60, TimeUnit.SECONDS); - } catch (InterruptedException e) { - logger.error("Interrupted", e); - Thread.currentThread().interrupt(); - return; - } catch (ExecutionException | TimeoutException e) { - logger.error("Exception in Rebalance.copyData", e); - return; - } - - makeTestBucket(bucketSnapshot); - } - - private void makeTestBucket(final Bucket[] bucketSnapshot) { - /* - * Now, create a 'TestBucket' table that mirrors the 'Bucket' table. - * Unlike the standard 'Bucket' and 'Server' tables, the 'TestServer' - * entries contain links referring back to the 'TestBucket' entries. - * This information is useful when rebalancing. - */ - for (Bucket bucket : bucketSnapshot) { - int index = bucket.index; - TestBucket testBucket = new TestBucket(index); - - // populate the 'owner' field - if (bucket.getOwner() != null) { - testBucket.setOwner(testServers.get(bucket.getOwner().getUuid())); - } else { - testBucket.setOwner(nullServer); - } - - // populate the 'primaryBackup' field - if (bucket.primaryBackup != null) { - testBucket.setPrimaryBackup( - testServers.get(bucket.primaryBackup.getUuid())); - } else { - testBucket.setPrimaryBackup(nullServer); - } - - // populate the 'secondaryBackup' field - if (bucket.secondaryBackup != null) { - testBucket.setSecondaryBackup( - testServers.get(bucket.secondaryBackup.getUuid())); - } else { - testBucket.setSecondaryBackup(nullServer); - } - buckets[index] = testBucket; - } - } - - /** - * Allocate unowned 'TestBucket' entries across all of the 'TestServer' - * entries. When every 'TestBucket' has an owner, rebalance as needed, - * so the 'TestServer' entry with the most buckets has at most one more - * bucket than the 'TestServer' entry with the least. - */ - void allocateBuckets() { - /* - * the following 'Comparator' is used to control the order of the - * 'needBuckets' TreeSet: those with the fewest buckets allocated are - * at the head of the list. - */ - Comparator<TestServer> bucketCount = (s1, s2) -> { - int rval = s1.buckets.size() - s2.buckets.size(); - if (rval == 0) { - rval = Util.uuidComparator.compare(s1.uuid, s2.uuid); - } - return rval; - }; - - // sort servers according to the order in which they can - // take on ownership of buckets - TreeSet<TestServer> needBuckets = new TreeSet<>(bucketCount); - for (TestServer ts : testServers.values()) { - needBuckets.add(ts); - } - - // go through all of the unowned buckets, and allocate them - for (TestBucket bucket : new LinkedList<TestBucket>(nullServer.buckets)) { - // take first entry off of sorted server list - TestServer ts = needBuckets.first(); - needBuckets.remove(ts); - - // add this bucket to the 'buckets' list - bucket.setOwner(ts); - - // place it back in the list, possibly in a new position - // (it's attributes have changed) - needBuckets.add(ts); - } - nullServer.buckets.clear(); - - // there may still be rebalancing needed -- no host should contain - // 2 or more buckets more than any other host - for ( ; ; ) { - TestServer first = needBuckets.first(); - TestServer last = needBuckets.last(); - - if (last.buckets.size() - first.buckets.size() <= 1) { - // no more rebalancing needed - break; - } - - // remove both from sorted list - needBuckets.remove(first); - needBuckets.remove(last); - - // take one bucket from 'last', and assign it to 'first' - last.buckets.first().setOwner(first); - - // place back in sorted list - needBuckets.add(first); - needBuckets.add(last); - } - } - - /** - * Make sure that the primary backups have the same site as the owner, - * and the secondary backups have a different site. - */ - void checkSiteValues() { - for (TestBucket bucket : buckets) { - if (bucket.owner != null) { - InetSocketAddress siteSocketAddress = - bucket.owner.siteSocketAddress; - TestServer primaryBackup = bucket.primaryBackup; - TestServer secondaryBackup = bucket.secondaryBackup; - - validateSiteOwner(bucket, siteSocketAddress, - primaryBackup, secondaryBackup); - } - } - } - - /** - * Validate primary site owner and secondary site owner are valid. - * @param bucket TestBucket - * @param siteSocketAddress site socket address - * @param primaryBackup primary backups - * @param secondaryBackup secondary backups - */ - private void validateSiteOwner(TestBucket bucket, InetSocketAddress siteSocketAddress, - TestServer primaryBackup, TestServer secondaryBackup) { - if (primaryBackup != null - && !Objects.equals(siteSocketAddress, - primaryBackup.siteSocketAddress)) { - /* - * primary backup is from the wrong site -- see if we can - * use the secondary. - */ - if (secondaryBackup != null - && Objects.equals(siteSocketAddress, - secondaryBackup.siteSocketAddress)) { - // swap primary and secondary - bucket.setPrimaryBackup(secondaryBackup); - bucket.setSecondaryBackup(primaryBackup); - } else { - // just invalidate primary backup - bucket.setPrimaryBackup(null); - } - } else if (secondaryBackup != null - && Objects.equals(siteSocketAddress, - secondaryBackup.siteSocketAddress)) { - // secondary backup is from the wrong site - bucket.setSecondaryBackup(null); - if (primaryBackup == null) { - // we can use this as the primary - bucket.setPrimaryBackup(secondaryBackup); - } - } - } - - /** - * Allocate and rebalance the primary backups. - */ - void rebalancePrimaryBackups() { - for (TestServer failedServer : testServers.values()) { - /* - * to allocate primary backups for this server, - * simulate a failure, and balance the backup hosts - */ - - // get siteSocketAddress from server - InetSocketAddress siteSocketAddress = failedServer.siteSocketAddress; - - // populate a 'TreeSet' of 'AdjustedTestServer' instances based - // the failure of 'failedServer' - TreeSet<AdjustedTestServer> adjustedTestServers = new TreeSet<>(); - for (TestServer server : testServers.values()) { - if (server == failedServer - || !Objects.equals(siteSocketAddress, - server.siteSocketAddress)) { - continue; - } - adjustedTestServers.add(new AdjustedTestServer(server, failedServer)); - } - - if (adjustedTestServers.isEmpty()) { - // this is presumably the only server -- there is no other server - // to act as primary backup, and no rebalancing can occur - continue; - } - - // we need a backup host for each bucket - for (TestBucket bucket : failedServer.buckets) { - if (bucket.primaryBackup == null - || bucket.primaryBackup == nullServer) { - // need a backup host for this bucket -- remove the first - // entry from 'adjustedTestServers', which is most favored - AdjustedTestServer backupHost = adjustedTestServers.first(); - adjustedTestServers.remove(backupHost); - - // update add this bucket to the list - bucket.setPrimaryBackup(backupHost.server); - - // update counts in 'AdjustedTestServer' - backupHost.bucketCount += 1; - backupHost.primaryBackupBucketCount += 1; - - // place it back in the table, possibly in a new position - // (it's attributes have changed) - adjustedTestServers.add(backupHost); - } - } - - // TBD: Is additional rebalancing needed? - } - } - - /** - * Allocate and rebalance the secondary backups. - */ - void rebalanceSecondaryBackups() { - for (TestServer failedServer : testServers.values()) { - /* - * to allocate secondary backups for this server, - * simulate a failure, and balance the backup hosts - */ - - // get siteSocketAddress from server - InetSocketAddress siteSocketAddress = failedServer.siteSocketAddress; - - // populate a 'TreeSet' of 'AdjustedTestServer' instances based - // the failure of 'failedServer' - TreeSet<AdjustedTestServer> adjustedTestServers = - new TreeSet<>(); - for (TestServer server : testServers.values()) { - if (server == failedServer - || Objects.equals(siteSocketAddress, - server.siteSocketAddress)) { - continue; - } - adjustedTestServers.add(new AdjustedTestServer(server, failedServer)); - } - - if (adjustedTestServers.isEmpty()) { - // this is presumably the only server -- there is no other server - // to act as secondary backup, and no rebalancing can occur - continue; - } - - // we need a backup host for each bucket - for (TestBucket bucket : failedServer.buckets) { - if (bucket.secondaryBackup == null - || bucket.secondaryBackup == nullServer) { - // need a backup host for this bucket -- remove the first - // entry from 'adjustedTestServers', which is most favored - AdjustedTestServer backupHost = adjustedTestServers.first(); - adjustedTestServers.remove(backupHost); - - // update add this bucket to the list - bucket.setSecondaryBackup(backupHost.server); - - // update counts in 'AdjustedTestServer' - backupHost.bucketCount += 1; - backupHost.secondaryBackupBucketCount += 1; - - // place it back in the table, possibly in a new position - // (it's attributes have changed) - adjustedTestServers.add(backupHost); - } - } - - // TBD: Is additional rebalancing needed? - } - } - - /** - * Generate a message with all of the bucket updates, process it locally, - * and send it to all servers in the "Notify List". - */ - void generateBucketMessage() throws IOException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - DataOutputStream dos = new DataOutputStream(bos); - - // go through the entire 'buckets' table - for (int i = 0; i < buckets.length; i += 1) { - // fetch the 'TestBucket' associated with index 'i' - TestBucket testBucket = buckets[i]; - - /* - * Get the UUID of the owner, primary backup, and secondary backup - * for this bucket. If the associated value does not exist, 'null' - * is used. - */ - UUID newOwner = null; - UUID newPrimary = null; - UUID newSecondary = null; - - if (testBucket.owner != nullServer && testBucket.owner != null) { - newOwner = testBucket.owner.uuid; - } - if (testBucket.primaryBackup != nullServer - && testBucket.primaryBackup != null) { - newPrimary = testBucket.primaryBackup.uuid; - } - if (testBucket.secondaryBackup != nullServer - && testBucket.secondaryBackup != null) { - newSecondary = testBucket.secondaryBackup.uuid; - } - - // write bucket number - dos.writeShort(i); - - // 'owner' field - writeOwner(dos, newOwner); - - // 'primaryBackup' field - writePrimary(dos, newPrimary); - - // 'secondaryBackup' field - writeSecondary(dos, newSecondary); - - dos.writeByte(END_OF_PARAMETERS_TAG); - } - - // get the unencoded 'packet' - final byte[] packet = bos.toByteArray(); - - // create an 'Entity' containing the encoded packet - final Entity<String> entity = - Entity.entity(new String(Base64.getEncoder().encode(packet), - StandardCharsets.UTF_8), MediaType.APPLICATION_OCTET_STREAM_TYPE); - /* - * This method is running within the 'MainLoop' thread. - */ - Runnable task = () -> { - try { - /* - * update the buckets on this host, - * which is presumably the lead server. - */ - Bucket.updateBucketInternal(packet); - } catch (Exception e) { - logger.error("Exception updating buckets", e); - } - - // send a message to all servers on the notify list - for (Server server : Server.getNotifyList()) { - server.post("bucket/update", entity); - } - }; - MainLoop.queueWork(task); - } - - private void writeOwner(DataOutputStream dos, UUID newOwner) throws IOException { - if (newOwner != null) { - dos.writeByte(OWNER_UPDATE); - Util.writeUuid(dos, newOwner); - } else { - dos.writeByte(OWNER_NULL); - } - } - - private void writePrimary(DataOutputStream dos, UUID newPrimary) throws IOException { - if (newPrimary != null) { - dos.writeByte(PRIMARY_BACKUP_UPDATE); - Util.writeUuid(dos, newPrimary); - } else { - dos.writeByte(PRIMARY_BACKUP_NULL); - } - } - - private void writeSecondary(DataOutputStream dos, UUID newSecondary) throws IOException { - if (newSecondary != null) { - dos.writeByte(SECONDARY_BACKUP_UPDATE); - Util.writeUuid(dos, newSecondary); - } else { - dos.writeByte(SECONDARY_BACKUP_NULL); - } - } - - /** - * Supports the '/cmd/dumpBuckets' REST message -- this isn't part of - * a 'rebalance' operation, but it turned out to be a convenient way - * to dump out the bucket table. - * - * @param out the output stream - */ - private void dumpBucketsInternal(PrintStream out) { - // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxx * - // UUID Type Buckets - String format = "%-36s %-9s %5s %s\n"; - - int totalOwner = 0; - int totalPrimary = 0; - int totalSecondary = 0; - - out.printf(format, "UUID", "Type", "Count", "Buckets"); - out.printf(format, "----", "----", "-----", "-------"); - for (TestServer ts : testServers.values()) { - // dump out 'owned' bucket information - if (ts.buckets.isEmpty()) { - // no buckets owned by this server - out.printf(format, ts.uuid, OWNED_STR, 0, ""); - } else { - // dump out primary buckets information - totalOwner += - dumpBucketsSegment(out, format, ts.buckets, ts.uuid.toString(), OWNED_STR); - } - // optionally dump out primary buckets information - totalPrimary += - dumpBucketsSegment(out, format, ts.primaryBackupBuckets, "", "Primary"); - // optionally dump out secondary buckets information - totalSecondary += - dumpBucketsSegment(out, format, ts.secondaryBackupBuckets, "", "Secondary"); - } - - if (!nullServer.buckets.isEmpty() - || !nullServer.primaryBackupBuckets.isEmpty() - || !nullServer.secondaryBackupBuckets.isEmpty()) { - /* - * There are some owned, primary, or secondary buckets that are - * unassigned. It is displayed in a manner similar to buckets that - * do have a server, but the UUID field is marked as 'UNASSIGNED' - * in the first line of the display. - */ - String uuidField = "UNASSIGNED"; - - // optionally dump out unassigned owned buckets information - if (dumpBucketsSegment(out, format, nullServer.buckets, - uuidField, OWNED_STR) != 0) { - uuidField = ""; - } - // optionally dump out unassigned primary backup buckets information - if (dumpBucketsSegment(out, format, nullServer.primaryBackupBuckets, - uuidField, "Primary") != 0) { - uuidField = ""; - } - // optionally dump out unassigned secondary backup buckets information - dumpBucketsSegment(out, format, nullServer.secondaryBackupBuckets, - uuidField, "Secondary"); - } - out.println("\nTotal assigned: owner = " + totalOwner - + ", primary = " + totalPrimary - + ", secondary = " + totalSecondary); - } - - /** - * Supports the 'dumpBucketsInternal' command, and indirectly, the - * '/cmd/dumpBuckets' REST message. It formats one segment of bucket data - * (owned, primary backup, or secondary backup), and dumps out the - * associated bucket data in segments of 8. Note: if the size of 'buckets' - * is 0, nothing is displayed. - * - * @param out the output stream - * @param format the message format string - * @param buckets the entire set of buckets to be displayed - * @param uuid string to display under the 'UUID' header - * @param segmentDescription string to display under the 'Type' header - * @return the size of the 'buckets' set - */ - private static int dumpBucketsSegment( - PrintStream out, String format, TreeSet<TestBucket> buckets, - String uuid, String segmentDescription) { - - int size = buckets.size(); - if (size != 0) { - // generate a linked list of the bucket data to display - LinkedList<String> data = new LinkedList<>(); - StringBuilder sb = new StringBuilder(); - int count = 8; - - for (TestBucket bucket : buckets) { - if (sb.length() != 0) { - // this is not the first bucket in the line -- - // prepend a space - sb.append(' '); - } - - // add the bucket number - sb.append(String.format("%4s", bucket.index)); - count -= 1; - if (count <= 0) { - // filled up a row -- - // add it to the list, and start a new line - data.add(sb.toString()); - sb = new StringBuilder(); - count = 8; - } - } - if (sb.length() != 0) { - // there is a partial line remaining -- add it to the list - data.add(sb.toString()); - } - - /* - * The first line displayed includes the UUID and size information, - * and the first line of bucket data (owned, primary, or secondary). - * The remaining lines of bucket data are displayed alone, - * without any UUID or size information. - */ - out.printf(format, uuid, segmentDescription, buckets.size(), - data.removeFirst()); - while (!data.isEmpty()) { - out.printf(format, "", "", "", data.removeFirst()); - } - } - return size; - } - } - - /* ============================================================ */ - - /** - * This interface is an abstraction for all messages that are routed - * through buckets. It exists, so that messages may be queued while - * bucket migration is taking place, and it makes it possible to support - * multiple types of messages (routed UEB/DMAAP messages, or lock messages) - */ - public static interface Message { - /** - * Process the current message -- this may mean delivering it locally, - * or forwarding it. - */ - public void process(); - - /** - * Send the message to another host for processing. - * - * @param server the destination host (although it could end up being - * forwarded again) - * @param bucketNumber the bucket number determined by extracting the - * current message's keyword - */ - public void sendToServer(Server server, int bucketNumber); - } - - /* ============================================================ */ - - /** - * This interface implements a type of backup; for example, there is one - * for backing up Drools objects within sessions, and another for - * backing up lock data. - */ - public static interface Backup { - /** - * This method is called to add a 'Backup' instance to the registered list. - * - * @param backup an object implementing the 'Backup' interface - */ - public static void register(Backup backup) { - synchronized (backupList) { - if (!backupList.contains(backup)) { - backupList.add(backup); - } - } - } - - /** - * Generate Serializable backup data for the specified bucket. - * - * @param bucketNumber the bucket number to back up - * @return a Serializable object containing backkup data - */ - public Restore generate(int bucketNumber); - } - - /* ============================================================ */ - - /** - * Objects implementing this interface may be serialized, and restored - * on a different host. - */ - public static interface Restore extends Serializable { - /** - * Restore from deserialized data. - * - * @param bucketNumber the bucket number being restored - */ - void restore(int bucketNumber); - } - - /* ============================================================ */ - - /** - * This interface corresponds to a transient state within a Bucket. - */ - private interface State { - /** - * This method allows state-specific handling of the - * 'Bucket.forward()' methods - * - * @param message the message to be forwarded/processed - * @return a value of 'true' indicates the message has been "handled" - * (forwarded or queued), and 'false' indicates it has not, and needs - * to be handled locally. - */ - boolean forward(Message message); - - /** - * This method indicates that the current server is the new owner - * of the current bucket. - */ - void newOwner(); - - /** - * This method indicates that serialized data has been received, - * presumably from the old owner of the bucket. The data could correspond - * to Drools objects within sessions, as well as global locks. - * - * @param data serialized data associated with this bucket (at present, - * this is assumed to be complete, all within a single message) - */ - void bulkSerializedData(byte[] data); - } - - /* ============================================================ */ - - /** - * Each state instance is associated with a bucket, and is used when - * that bucket is in a transient state where it is the new owner of a - * bucket, or is presumed to be the new owner, based upon other events - * that have occurred. - */ - private class NewOwner extends Thread implements State { - /* - * this value is 'true' if we have explicitly received a 'newOwner' - * indication, and 'false' if there was another trigger for entering this - * transient state (e.g. receiving serialized data) - */ - boolean confirmed; - - // when 'System.currentTimeMillis()' reaches this value, we time out - long endTime; - - // If not 'null', we are queueing messages for this bucket - // otherwise, we are sending them through. - Queue<Message> messages = new ConcurrentLinkedQueue<>(); - - // this is used to signal the thread that we have data available - CountDownLatch dataAvailable = new CountDownLatch(1); - - // this is the data - byte[] data = null; - - // this is the old owner of the bucket - Server oldOwner; - - /** - * Constructor - a transient state, where we are expecting to receive - * bulk data from the old owner. - * - * @param confirmed 'true' if we were explicitly notified that we - * are the new owner of the bucket, 'false' if not - */ - NewOwner(boolean confirmed, Server oldOwner) { - super("New Owner for Bucket " + index); - this.confirmed = confirmed; - this.oldOwner = oldOwner; - if (oldOwner == null) { - // we aren't expecting any data -- this is indicated by 0-length data - bulkSerializedData(new byte[0]); - } - endTime = System.currentTimeMillis() - + (confirmed ? confirmedTimeout : unconfirmedTimeout); - start(); - } - - /** - * Return the 'confirmed' indicator. - * - * @return the 'confirmed' indicator - */ - private boolean getConfirmed() { - synchronized (Bucket.this) { - return confirmed; - } - } - - /** - * This returns the timeout delay, which will always be less than or - * equal to 1 second. This allows us to periodically check whether the - * old server is still active. - * - * @return the timeout delay, which is the difference between the - * 'endTime' value and the current time or 1 second - * (whichever is less) - */ - private long getTimeout() { - long lclEndTime; - synchronized (Bucket.this) { - lclEndTime = endTime; - } - return Math.min(lclEndTime - System.currentTimeMillis(), 1000L); - } - - /** - * Return the current value of the 'data' field. - * - * @return the current value of the 'data' field - */ - private byte[] getData() { - synchronized (Bucket.this) { - return data; - } - } - - /* ******************* */ - /* 'State' interface */ - /* ******************* */ - - /** - * {@inheritDoc} - */ - @Override - public boolean forward(Message message) { - // the caller of this method is synchronized on 'Bucket.this' - if (messages != null && Thread.currentThread() != this) { - // just queue the message - messages.add(message); - return true; - } else { - /* - * Either: - * - * 1) We are in a grace period, where 'state' is still set, but - * we are no longer forwarding messages. - * 2) We are calling 'message.process()' from this thread - * in the 'finally' block of 'NewOwner.run()'. - * - * In either case, messages should be processed locally. - */ - return false; - } - } - - /** - * {@inheritDoc} - */ - @Override - public void newOwner() { - // the caller of this method is synchronized on 'Bucket.this' - if (!confirmed) { - confirmed = true; - endTime += (confirmedTimeout - unconfirmedTimeout); - } - } - - /** - * {@inheritDoc} - */ - @Override - public void bulkSerializedData(byte[] data) { - // the caller of this method is synchronized on 'Bucket.this' - if (this.data == null) { - this.data = data; - dataAvailable.countDown(); - } - } - - /* ******************** */ - /* 'Thread' interface */ - /* ******************** */ - - /** - * {@inheritDoc} - */ - @Override - public void run() { - logger.info("{}: 'run' method invoked", this); - try { - byte[] lclData; - long delay; - - while ((lclData = getData()) == null - && oldOwner.isActive() - && (delay = getTimeout()) > 0) { - // ignore return value -- 'data' will indicate the result - if (!dataAvailable.await(delay, TimeUnit.MILLISECONDS)) { - logger.error("CountDownLatch await time reached"); - } - } - if (lclData == null) { - // no data available -- log an error, and abort - if (getConfirmed()) { - // we never received the data, but we are the new owner - logger.error("{}: never received session data", this); - } else { - /* - * no data received, and it was never confirmed -- - * assume that the forwarded message that triggered this was - * erroneus - */ - logger.error("{}: no confirmation or data received -- aborting", this); - return; - } - } else { - logger.info("{}: {} bytes of data available", - this, lclData.length); - } - - // if we reach this point, this server is the new owner - if (lclData == null || lclData.length == 0) { - // see if any features can do the restore - for (ServerPoolApi feature : ServerPoolApi.impl.getList()) { - feature.restoreBucket(Bucket.this); - } - } else { - // deserialize data - Object obj = Util.deserialize(lclData); - restoreBucketData(obj); - } - } catch (Exception e) { - logger.error("Exception in {}", this, e); - } finally { - runCleanup(); - } - } - - private void runCleanup() { - /* - * cleanly leave state -- we want to make sure that messages - * are processed in order, so the queue needs to remain until - * it is empty - */ - logger.info("{}: entering cleanup state", this); - for ( ; ; ) { - Message message = messages.poll(); - if (message == null) { - // no messages left, but this could change - synchronized (Bucket.this) { - message = messages.poll(); - if (message == null) { - // no messages left - noMoreMessages(); - break; - } - } - } - // this doesn't work -- it ends up right back in the queue - // if 'messages' is defined - message.process(); - } - if (messages == null) { - // this indicates we need to enter a grace period before cleanup, - sleepBeforeCleanup(); - } - logger.info("{}: exiting cleanup state", this); - } - - private void noMoreMessages() { - if (state == this) { - if (owner == Server.getThisServer()) { - // we can now exit the state - state = null; - stateChanged(); - } else { - /* - * We need a grace period before we can - * remove the 'state' value (this can happen - * if we receive and process the bulk data - * before receiving official confirmation - * that we are owner of the bucket. - */ - messages = null; - } - } - } - - private void sleepBeforeCleanup() { - try { - logger.info("{}: entering grace period before terminating", - this); - Thread.sleep(unconfirmedGracePeriod); - } catch (InterruptedException e) { - // we are exiting in any case - Thread.currentThread().interrupt(); - } finally { - synchronized (Bucket.this) { - // Do we need to confirm that we really are the owner? - // What does it mean if we are not? - if (state == this) { - state = null; - stateChanged(); - } - } - } - } - - /** - * Return a useful value to display in log messages. - * - * @return a useful value to display in log messages - */ - public String toString() { - return "Bucket.NewOwner(" + index + ")"; - } - - /** - * Restore bucket data. - * - * @param obj deserialized bucket data - */ - private void restoreBucketData(Object obj) { - if (obj instanceof List) { - for (Object entry : (List<?>) obj) { - if (entry instanceof Restore) { - // entry-specific 'restore' operation - ((Restore) entry).restore(Bucket.this.index); - } else { - logger.error("{}: Expected '{}' but got '{}'", - this, Restore.class.getName(), - entry.getClass().getName()); - } - } - } else { - logger.error("{}: expected 'List' but got '{}'", - this, obj.getClass().getName()); - } - } - } - - - /* ============================================================ */ - - /** - * Each state instance is associated with a bucket, and is used when - * that bucket is in a transient state where it is the old owner of - * a bucket, and the data is being transferred to the new owner. - */ - private class OldOwner extends Thread implements State { - Server newOwner; - - OldOwner(Server newOwner) { - super("Old Owner for Bucket " + index); - this.newOwner = newOwner; - start(); - } - - /* ******************* */ - /* 'State' interface */ - /* ******************* */ - - /** - * {@inheritDoc} - */ - @Override - public boolean forward(Message message) { - // forward message to new owner - message.sendToServer(newOwner, index); - return true; - } - - /** - * {@inheritDoc} - */ - @Override - public void newOwner() { - // shouldn't happen -- just log an error - logger.error("{}: 'newOwner()' shouldn't be called in this state", this); - } - - /** - * {@inheritDoc} - */ - @Override - public void bulkSerializedData(byte[] data) { - // shouldn't happen -- just log an error - logger.error("{}: 'bulkSerializedData()' shouldn't be called in this state", this); - } - - /* ******************** */ - /* 'Thread' interface */ - /* ******************** */ - - /** - * {@inheritDoc} - */ - @Override - public void run() { - logger.info("{}: 'run' method invoked", this); - try { - // go through all of the entries in the list, collecting restore data - List<Restore> restoreData = new LinkedList<>(); - for (Backup backup : backupList) { - Restore restore = backup.generate(index); - if (restore != null) { - restoreData.add(restore); - } - } - - // serialize all of the objects, - // and send what we have to the new owner - Entity<String> entity = Entity.entity( - new String(Base64.getEncoder().encode(Util.serialize(restoreData))), - MediaType.APPLICATION_OCTET_STREAM_TYPE); - newOwner.post("bucket/sessionData", entity, new Server.PostResponse() { - @Override - public WebTarget webTarget(WebTarget webTarget) { - return webTarget - .queryParam(QP_BUCKET, index) - .queryParam(QP_DEST, newOwner.getUuid()) - .queryParam(QP_TTL, timeToLive); - } - - @Override - public void response(Response response) { - logger.info("/bucket/sessionData response code = {}", - response.getStatus()); - } - }); - } catch (Exception e) { - logger.error("Exception in {}", this, e); - } finally { - synchronized (Bucket.this) { - // restore the state - if (state == this) { - state = null; - stateChanged(); - } - } - } - } - - /** - * Return a useful value to display in log messages. - * - * @return a useful value to display in log messages - */ - public String toString() { - return "Bucket.OldOwner(" + index + ")"; - } - } -} diff --git a/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/Discovery.java b/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/Discovery.java deleted file mode 100644 index a53fb4d1..00000000 --- a/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/Discovery.java +++ /dev/null @@ -1,352 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * feature-server-pool - * ================================================================================ - * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved. - * ================================================================================ - * 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. - * ============LICENSE_END========================================================= - */ - -package org.onap.policy.drools.serverpool; - -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DEFAULT_DISCOVERY_FETCH_LIMIT; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DEFAULT_DISCOVERY_FETCH_TIMEOUT; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DEFAULT_DISCOVER_PUBLISHER_LOOP_CYCLE_TIME; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DISCOVERY_ALLOW_SELF_SIGNED_CERTIFICATES; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DISCOVERY_API_KEY; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DISCOVERY_API_SECRET; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DISCOVERY_FETCH_LIMIT; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DISCOVERY_FETCH_TIMEOUT; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DISCOVERY_HTTPS; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DISCOVERY_PASSWORD; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DISCOVERY_SERVERS; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DISCOVERY_TOPIC; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DISCOVERY_USERNAME; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DISCOVER_PUBLISHER_LOOP_CYCLE_TIME; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.getProperty; - -import com.google.gson.Gson; -import java.io.ByteArrayOutputStream; -import java.io.DataOutputStream; -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import org.onap.policy.common.endpoints.event.comm.Topic.CommInfrastructure; -import org.onap.policy.common.endpoints.event.comm.TopicEndpointManager; -import org.onap.policy.common.endpoints.event.comm.TopicListener; -import org.onap.policy.common.endpoints.event.comm.TopicSink; -import org.onap.policy.common.endpoints.event.comm.TopicSource; -import org.onap.policy.common.endpoints.properties.PolicyEndPointProperties; -import org.onap.policy.common.utils.coder.CoderException; -import org.onap.policy.common.utils.coder.StandardCoder; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * This class makes use of UEB/DMAAP to discover other servers in the pool. - * The discovery processes ordinarily run only on the lead server, but they - * run on other servers up until the point that they determine who the - * leader is. - */ -public class Discovery implements TopicListener { - private static Logger logger = LoggerFactory.getLogger(Discovery.class); - - // used for JSON <-> String conversion - private static StandardCoder coder = new StandardCoder(); - - private static Discovery discovery = null; - - private volatile Publisher publisherThread = null; - - private List<TopicSource> consumers = null; - private List<TopicSink> publishers = null; - - private Discovery() { - // we want to modify the properties we send to 'TopicManager' - PropBuilder builder = new PropBuilder(ServerPoolProperties.getProperties()); - builder.convert(DISCOVERY_SERVERS, null, - PolicyEndPointProperties.PROPERTY_TOPIC_SERVERS_SUFFIX); - builder.convert(DISCOVERY_USERNAME, null, - PolicyEndPointProperties.PROPERTY_TOPIC_AAF_MECHID_SUFFIX); - builder.convert(DISCOVERY_PASSWORD, null, - PolicyEndPointProperties.PROPERTY_TOPIC_AAF_PASSWORD_SUFFIX); - builder.convert(DISCOVERY_HTTPS, null, - PolicyEndPointProperties.PROPERTY_HTTP_HTTPS_SUFFIX); - builder.convert(DISCOVERY_API_KEY, null, - PolicyEndPointProperties.PROPERTY_TOPIC_API_KEY_SUFFIX); - builder.convert(DISCOVERY_API_SECRET, null, - PolicyEndPointProperties.PROPERTY_TOPIC_API_SECRET_SUFFIX); - builder.convert(DISCOVERY_FETCH_TIMEOUT, - String.valueOf(DEFAULT_DISCOVERY_FETCH_TIMEOUT), - PolicyEndPointProperties.PROPERTY_TOPIC_SOURCE_FETCH_TIMEOUT_SUFFIX); - builder.convert(DISCOVERY_FETCH_LIMIT, - String.valueOf(DEFAULT_DISCOVERY_FETCH_LIMIT), - PolicyEndPointProperties.PROPERTY_TOPIC_SOURCE_FETCH_LIMIT_SUFFIX); - builder.convert(DISCOVERY_ALLOW_SELF_SIGNED_CERTIFICATES, null, - PolicyEndPointProperties.PROPERTY_ALLOW_SELF_SIGNED_CERTIFICATES_SUFFIX); - Properties prop = builder.finish(); - logger.debug("Discovery converted properties: {}", prop); - - consumers = TopicEndpointManager.getManager().addTopicSources(prop); - publishers = TopicEndpointManager.getManager().addTopicSinks(prop); - - if (consumers.isEmpty()) { - logger.error("No consumer topics"); - } - if (publishers.isEmpty()) { - logger.error("No publisher topics"); - } - logger.debug("Discovery: {} consumers, {} publishers", - consumers.size(), publishers.size()); - } - - /** - * Start all consumers and publishers, and start the publisher thread. - */ - static synchronized void startDiscovery() { - if (discovery == null) { - discovery = new Discovery(); - } - discovery.start(); - } - - /** - * Stop all consumers and publishers, and stop the publisher thread. - */ - static synchronized void stopDiscovery() { - if (discovery != null) { - discovery.stop(); - } - } - - /** - * Start all consumers and publishers, and start the publisher thread. - */ - private void start() { - for (TopicSource consumer : consumers) { - consumer.register(this); - consumer.start(); - } - for (TopicSink publisher : publishers) { - publisher.start(); - } - if (publisherThread == null) { - // send thread wasn't running -- start it - publisherThread = new Publisher(); - publisherThread.start(); - } - } - - /** - * Stop all consumers and publishers, and stop the publisher thread. - */ - private void stop() { - publisherThread = null; - for (TopicSink publisher : publishers) { - publisher.stop(); - } - for (TopicSource consumer : consumers) { - consumer.unregister(this); - consumer.stop(); - } - } - - /*===========================*/ - /* 'TopicListener' interface */ - /*===========================*/ - - /** - * {@inheritDoc} - */ - @Override - public void onTopicEvent(CommInfrastructure infra, String topic, String event) { - /* - * a JSON message has been received -- it should contain - * a single string parameter 'pingData', which contains the - * same format base64-encoded message that 'Server' - * instances periodically exchange - */ - try { - @SuppressWarnings("unchecked") - LinkedHashMap<String, String> map = coder.decode(event, LinkedHashMap.class); - String message = map.get("pingData"); - Server.adminRequest(message.getBytes(StandardCharsets.UTF_8)); - logger.info("Received a message, server count={}", Server.getServerCount()); - } catch (CoderException e) { - logger.error("Can't decode message", e); - } - } - - /* ============================================================ */ - - /** - * This class is used to convert internal 'discovery.*' properties to - * properties that 'TopicEndpointManager' can use. - */ - private static class PropBuilder { - // properties being incrementally modified - Properties prop; - - // value from 'discovery.topic' parameter - String topic; - - // 'true' only if both 'discovery.topic' and 'discovery.servers' - // has been defined - boolean doConversion = false; - - // contains "ueb.source.topics" or "dmaap.source.topics" - String sourceTopicsName = null; - - // contains "<TYPE>.source.topics.<TOPIC>" (<TYPE> = ueb|dmaap) - String sourcePrefix = null; - - // contains "ueb.sink.topics" or "dmaap.sink.topics" - String sinkTopicsName = null; - - // contains "<TYPE>.sink.topics.<TOPIC>" (<TYPE> = ueb|dmaap) - String sinkPrefix = null; - - /** - * Constructor - decide whether we are going to do conversion or not, - * and initialize accordingly. - * - * @param prop the initial list of properties - */ - PropBuilder(Properties prop) { - this.prop = new Properties(prop); - this.topic = prop.getProperty(DISCOVERY_TOPIC); - String servers = prop.getProperty(DISCOVERY_SERVERS); - if (topic != null && servers != null) { - // we do have property conversion to do - doConversion = true; - String type = topic.contains(".") ? "dmaap" : "ueb"; - sourceTopicsName = type + ".source.topics"; - sourcePrefix = sourceTopicsName + "." + topic; - sinkTopicsName = type + ".sink.topics"; - sinkPrefix = sinkTopicsName + "." + topic; - } - } - - /** - * If we are doing conversion, convert an internal property - * to something that 'TopicEndpointManager' can use. - * - * @param intName server pool property name (e.g. "discovery.servers") - * @param defaultValue value to use if property 'intName' is not specified - * @param extSuffix TopicEndpointManager suffix, including leading "." - */ - void convert(String intName, String defaultValue, String extSuffix) { - if (doConversion) { - String value = prop.getProperty(intName, defaultValue); - if (value != null) { - prop.setProperty(sourcePrefix + extSuffix, value); - prop.setProperty(sinkPrefix + extSuffix, value); - } - } - } - - /** - * Generate/update the '*.source.topics' and '*.sink.topics' parameters. - * - * @return the updated properties list - */ - Properties finish() { - if (doConversion) { - String currentValue = prop.getProperty(sourceTopicsName); - if (currentValue == null) { - // '*.source.topics' is not defined -- set it - prop.setProperty(sourceTopicsName, topic); - } else { - // '*.source.topics' is defined -- append to it - prop.setProperty(sourceTopicsName, currentValue + "," + topic); - } - currentValue = prop.getProperty(sinkTopicsName); - if (currentValue == null) { - // '*.sink.topics' is not defined -- set it - prop.setProperty(sinkTopicsName, topic); - } else { - // '*.sink.topics' is defined -- append to it - prop.setProperty(sinkTopicsName, currentValue + "," + topic); - } - } - return prop; - } - } - - /* ============================================================ */ - - /** - * This is the sender thread, which periodically sends out 'ping' messages. - */ - private class Publisher extends Thread { - /** - * Constructor -- read in the properties, and initialze 'publisher'. - */ - Publisher() { - super("Discovery Publisher Thread"); - } - - /** - * {@inheritDoc} - */ - @Override - public void run() { - // this loop will terminate once 'publisher' is set to 'null', - // or some other 'Publisher' instance replaces it - long cycleTime = getProperty(DISCOVER_PUBLISHER_LOOP_CYCLE_TIME, - DEFAULT_DISCOVER_PUBLISHER_LOOP_CYCLE_TIME); - while (this == publisherThread) { - try { - // wait 5 seconds (default) - Thread.sleep(cycleTime); - - // generate a 'ping' message - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - DataOutputStream dos = new DataOutputStream(bos); - - // write the 'ping' data for this server - Server thisServer = Server.getThisServer(); - thisServer.writeServerData(dos); - String encodedData = - new String(Base64.getEncoder().encode(bos.toByteArray())); - - // base64-encoded value is passed as JSON parameter 'pingData' - LinkedHashMap<String, String> map = new LinkedHashMap<>(); - map.put("pingData", encodedData); - String jsonString = new Gson().toJson(map, Map.class); - for (TopicSink publisher : publishers) { - publisher.send(jsonString); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - logger.error("Exception in Discovery.Publisher.run():", e); - return; - } catch (Exception e) { - logger.error("Exception in Discovery.Publisher.run():", e); - // grace period -- we don't want to get UEB upset at us - try { - Thread.sleep(15000); - } catch (InterruptedException e2) { - Thread.currentThread().interrupt(); - logger.error("Discovery.Publisher sleep interrupted"); - } - return; - } - } - } - } -} diff --git a/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/Events.java b/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/Events.java deleted file mode 100644 index d2ea1a5c..00000000 --- a/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/Events.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * feature-server-pool - * ================================================================================ - * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved. - * ================================================================================ - * 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. - * ============LICENSE_END========================================================= - */ - -package org.onap.policy.drools.serverpool; - -import java.util.Collection; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; - -/** - * This interface is used to distribute notifications of various system - * events, such as new 'Server' instances, or a server failing. - */ -public class Events { - // set of listeners receiving event notifications - private static final Queue<Events> listeners = - new ConcurrentLinkedQueue<>(); - - /** - * add a listener to the set of listeners receiving events. - * - * @param handler the listener - */ - public static void register(Events handler) { - // if it is already here, remove it first - listeners.remove(handler); - - // add to the end of the queue - listeners.add(handler); - } - - /** - * remove a listener from the set of listeners. - */ - public static boolean unregister(Events handler) { - return listeners.remove(handler); - } - - public static Collection<Events> getListeners() { - return listeners; - } - - /* ============================================================ */ - - /** - * Notification that a new server has been discovered. - * - * @param server this is the new server - */ - public void newServer(Server server) { - // do nothing - } - - /** - * Notification that a server has failed. - * - * @param server this is the server that failed - */ - public void serverFailed(Server server) { - // do nothing - } - - /** - * Notification that a new lead server has been selected. - * - * @param server this is the new lead server - */ - public void newLeader(Server server) { - // do nothing - } - - /** - * Notification that the lead server has gone down. - * - * @param server the lead server that failed - */ - public void leaderFailed(Server server) { - // do nothing - } - - /** - * Notification that a new selection just completed, but the same - * leader has been chosen (this may be in response to a new server - * joining earlier). - * - * @param server the current leader, which has been confirmed - */ - public void leaderConfirmed(Server server) { - // do nothing - } -} diff --git a/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/ExtendedObjectInputStream.java b/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/ExtendedObjectInputStream.java deleted file mode 100644 index 5ec6f341..00000000 --- a/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/ExtendedObjectInputStream.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * feature-server-pool - * ================================================================================ - * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved. - * ================================================================================ - * 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. - * ============LICENSE_END========================================================= - */ - -package org.onap.policy.drools.serverpool; - -import java.io.IOException; -import java.io.InputStream; -import java.io.ObjectInputStream; -import java.io.ObjectStreamClass; - -/** - * This class provides an 'ObjectInputStream' variant that uses the - * specified 'ClassLoader' instance. - */ -public class ExtendedObjectInputStream extends ObjectInputStream { - // the 'ClassLoader' to use when doing class lookups - private ClassLoader classLoader; - - /** - * Constructor -- invoke the superclass, and save the 'ClassLoader'. - * - * @param in input stream to read from - * @param classLoader 'ClassLoader' to use when doing class lookups - */ - public ExtendedObjectInputStream(InputStream in, ClassLoader classLoader) throws IOException { - super(in); - this.classLoader = classLoader; - } - - /** - * {@inheritDoc} - */ - @Override - protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { - - // Standard ClassLoader implementations first attempt to load classes - // via the parent class loader, and then attempt to load it using the - // current class loader if that fails. For some reason, Drools container - // class loaders define a different order -- in theory, this is only a - // problem if different versions of the same class are accessible through - // different class loaders, which is exactly what happens in some Junit - // tests. - // - // This change restores the order, at least when deserializing objects - // into a Drools container. - try { - // try the parent class loader first - return classLoader.getParent().loadClass(desc.getName()); - } catch (ClassNotFoundException e) { - return classLoader.loadClass(desc.getName()); - } - } -} diff --git a/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/FeatureServerPool.java b/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/FeatureServerPool.java deleted file mode 100644 index 064af79e..00000000 --- a/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/FeatureServerPool.java +++ /dev/null @@ -1,1027 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * feature-server-pool - * ================================================================================ - * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved. - * Modifications Copyright (C) 2020 Nordix Foundation - * ================================================================================ - * 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. - * ============LICENSE_END========================================================= - */ - -package org.onap.policy.drools.serverpool; - -import static org.onap.policy.drools.serverpool.ServerPoolProperties.BUCKET_DROOLS_TIMEOUT; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.BUCKET_TIME_TO_LIVE; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DEFAULT_BUCKET_DROOLS_TIMEOUT; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DEFAULT_BUCKET_TIME_TO_LIVE; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.getProperty; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.Serializable; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Base64; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Properties; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import javax.ws.rs.client.Entity; -import javax.ws.rs.client.WebTarget; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import lombok.AllArgsConstructor; -import org.apache.commons.lang3.tuple.Pair; -import org.kie.api.runtime.KieSession; -import org.kie.api.runtime.rule.FactHandle; -import org.onap.policy.common.endpoints.event.comm.Topic.CommInfrastructure; -import org.onap.policy.common.endpoints.event.comm.TopicListener; -import org.onap.policy.common.utils.coder.CoderException; -import org.onap.policy.common.utils.coder.StandardCoder; -import org.onap.policy.common.utils.coder.StandardCoderObject; -import org.onap.policy.drools.control.api.DroolsPdpStateControlApi; -import org.onap.policy.drools.core.DroolsRunnable; -import org.onap.policy.drools.core.PolicyContainer; -import org.onap.policy.drools.core.PolicySession; -import org.onap.policy.drools.core.PolicySessionFeatureApi; -import org.onap.policy.drools.core.lock.PolicyResourceLockManager; -import org.onap.policy.drools.features.PolicyControllerFeatureApi; -import org.onap.policy.drools.features.PolicyEngineFeatureApi; -import org.onap.policy.drools.system.PolicyController; -import org.onap.policy.drools.system.PolicyControllerConstants; -import org.onap.policy.drools.system.PolicyEngine; -import org.onap.policy.drools.system.PolicyEngineConstants; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * </p> - * This class hooks the server pool implementation into DroolsPDP. - * <dl> - * <dt>PolicyEngineFeatureApi</dt><dd> - the <i>afterStart</i> hook is where we initialize.</dd> - * <dt>PolicyControllerFeatureApi</dt><dd> - the <i>beforeOffer</i> hook is used to look - * at incoming topic messages, and decide whether to process them - * on this host, or forward to another host.</dd> - * </dl> - */ -public class FeatureServerPool - implements PolicyEngineFeatureApi, PolicySessionFeatureApi, - PolicyControllerFeatureApi, DroolsPdpStateControlApi { - private static Logger logger = - LoggerFactory.getLogger(FeatureServerPool.class); - - // used for JSON <-> String conversion - private static StandardCoder coder = new StandardCoder(); - - private static final String CONFIG_FILE = - "config/feature-server-pool.properties"; - - /* - * Properties used when searching for keyword entries - * - * The following types are supported: - * - * 1) keyword.<topic>.path=<field-list> - * 2) keyword.path=<field-list> - * 3) ueb.source.topics.<topic>.keyword=<field-list> - * 4) ueb.source.topics.keyword=<field-list> - * 5) dmaap.source.topics.<topic>.keyword=<field-list> - * 6) dmaap.source.topics.keyword=<field-list> - * - * 1, 3, and 5 are functionally equivalent - * 2, 4, and 6 are functionally equivalent - */ - - static final String KEYWORD_PROPERTY_START_1 = "keyword."; - static final String KEYWORD_PROPERTY_END_1 = ".path"; - static final String KEYWORD_PROPERTY_START_2 = "ueb.source.topics."; - static final String KEYWORD_PROPERTY_END_2 = ".keyword"; - static final String KEYWORD_PROPERTY_START_3 = "dmaap.source.topics."; - static final String KEYWORD_PROPERTY_END_3 = ".keyword"; - - /* - * maps topic names to a keyword table derived from <field-list> (above) - * - * Example <field-list>: requestID,CommonHeader.RequestID - * - * Table generated from this example has length 2: - * table 0 is "requestID" - * table 1 is "CommonHeader", "RequestID" - */ - private static HashMap<String, String[][]> topicToPaths = new HashMap<>(); - - // this table is used for any topics that aren't in 'topicToPaths' - private static String[][] defaultPaths = new String[0][]; - - // extracted from properties - private static long droolsTimeoutMillis; - private static String timeToLiveSecond; - - // HTTP query parameters - private static final String QP_KEYWORD = "keyword"; - private static final String QP_SESSION = "session"; - private static final String QP_BUCKET = "bucket"; - private static final String QP_TTL = "ttl"; - private static final String QP_CONTROLLER = "controller"; - private static final String QP_PROTOCOL = "protocol"; - private static final String QP_TOPIC = "topic"; - - /* **************************** */ - /* 'OrderedService' interface */ - /* **************************** */ - - /** - * {@inheritDoc} - */ - @Override - public int getSequenceNumber() { - // we need to make sure we have an early position in 'selectThreadModel' - // (in case there is feature that provides a thread model) - return -1000000; - } - - /* ************************************ */ - /* 'PolicyEngineFeatureApi' interface */ - /* ************************************ */ - - /** - * {@inheritDoc} - */ - @Override - public boolean afterStart(PolicyEngine engine) { - logger.info("Starting FeatureServerPool"); - Server.startup(CONFIG_FILE); - TargetLock.startup(); - setDroolsTimeoutMillis( - getProperty(BUCKET_DROOLS_TIMEOUT, DEFAULT_BUCKET_DROOLS_TIMEOUT)); - int intTimeToLive = - getProperty(BUCKET_TIME_TO_LIVE, DEFAULT_BUCKET_TIME_TO_LIVE); - setTimeToLiveSecond(String.valueOf(intTimeToLive)); - buildKeywordTable(); - Bucket.Backup.register(new DroolsSessionBackup()); - Bucket.Backup.register(new TargetLock.LockBackup()); - return false; - } - - private static void setDroolsTimeoutMillis(long timeoutMs) { - droolsTimeoutMillis = timeoutMs; - } - - private static void setTimeToLiveSecond(String ttlSec) { - timeToLiveSecond = ttlSec; - } - - /** - * {@inheritDoc} - */ - @Override - public PolicyResourceLockManager beforeCreateLockManager( - PolicyEngine engine, Properties properties) { - - return TargetLock.getLockFactory(); - } - - /*=====================================*/ - /* 'PolicySessionFeatureApi' interface */ - /*=====================================*/ - - /** - * {@inheritDoc} - */ - @Override - public boolean insertDrools(final PolicySession session, final Object object) { // NOSONAR - // sonar complained that the method always returns the same value. However, - // we prefer the code be structured this way, thus disabled sonar - - final String keyword = Keyword.lookupKeyword(object); - if (keyword == null) { - // no keyword was found, so we process locally - KieSession kieSession = session.getKieSession(); - if (kieSession != null) { - kieSession.insert(object); - } - return true; - } - - /* - * 'keyword' determines the destination host, - * which may be local or remote - */ - Bucket.forwardAndProcess(keyword, new Bucket.Message() { - @Override - public void process() { - // if we reach this point, we process locally - KieSession kieSession = session.getKieSession(); - if (kieSession != null) { - kieSession.insert(object); - } - } - - @Override - public void sendToServer(Server server, int bucketNumber) { - // this object needs to sent to a remote host -- - // first, serialize the object - byte[] data = null; - try { - data = Util.serialize(object); - } catch (IOException e) { - logger.error("insertDrools: can't serialize object of {}", - object.getClass(), e); - return; - } - - // construct the message to insert remotely - Entity<String> entity = Entity.entity( - new String(Base64.getEncoder().encode(data), StandardCharsets.UTF_8), - MediaType.APPLICATION_OCTET_STREAM_TYPE); - server.post("session/insertDrools", entity, - new Server.PostResponse() { - @Override - public WebTarget webTarget(WebTarget webTarget) { - PolicyContainer pc = session.getPolicyContainer(); - String encodedSessionName = - pc.getGroupId() + ":" + pc.getArtifactId() + ":" - + session.getName(); - - return webTarget - .queryParam(QP_KEYWORD, keyword) - .queryParam(QP_SESSION, encodedSessionName) - .queryParam(QP_BUCKET, bucketNumber) - .queryParam(QP_TTL, timeToLiveSecond); - } - - @Override - public void response(Response response) { - logger.info("/session/insertDrools response code = {}", - response.getStatus()); - } - }); - } - }); - return true; - } - - /* **************************************** */ - /* 'PolicyControllerFeatureApi' interface */ - /* **************************************** */ - - /** - * This method is called from 'AggregatedPolicyController.onTopicEvent', - * and provides a way to intercept the message before it is decoded and - * delivered to a local Drools session. - * - * @param controller the PolicyController instance receiving the message - * @param protocol communication infrastructure type - * @param topic topic name - * @param event event message as a string - * @return 'false' if this event should be processed locally, or 'true' - * if the message has been forwarded to a remote host, so local - * processing should be bypassed - */ - @Override - public boolean beforeOffer(final PolicyController controller, - final CommInfrastructure protocol, - final String topic, - final String event) { - // choose the table, based upon the topic - String[][] table = topicToPaths.getOrDefault(topic, defaultPaths); - - // build a JSON object from the event - StandardCoderObject sco; - - try { - sco = coder.decode(event, StandardCoderObject.class); - } catch (CoderException e) { - return false; - } - String keyword = null; - - for (String[] path : table) { - /* - * Each entry in 'table' is a String[] containing an encoding - * of a possible keyword field. Suppose the value is 'a.b.c.d.e' -- - * 'path' would be encoded as 'String[] {"a", "b", "c", "d", "e"}' - */ - String fieldName = path[path.length - 1]; - String conversionFunctionName = null; - int index = fieldName.indexOf(':'); - - if (index > 0) { - conversionFunctionName = fieldName.substring(index + 1); - fieldName = fieldName.substring(0, index); - path = Arrays.copyOf(path, path.length); - path[path.length - 1] = fieldName; - } - keyword = sco.getString((Object[]) path); - - if (keyword != null) { - if (conversionFunctionName != null) { - keyword = Keyword.convertKeyword(keyword, conversionFunctionName); - } - if (keyword != null) { - break; - } - } - } - - if (keyword == null) { - // couldn't find any keywords -- just process this message locally - logger.warn("Can't locate bucket keyword within message"); - return false; - } - - /* - * build a message object implementing the 'Bucket.Message' interface -- - * it will be processed locally, forwarded, or queued based upon the - * current state. - */ - TopicMessage message = - new TopicMessage(keyword, controller, protocol, topic, event); - int bucketNumber = Bucket.bucketNumber(keyword); - if (Bucket.forward(bucketNumber, message)) { - // message was queued or forwarded -- abort local processing - return true; - } - - /* - * the bucket happens to be assigned to this server, and wasn't queued -- - * return 'false', so it will be processed locally - */ - logger.info("Keyword={}, bucket={} -- owned by this server", - keyword, bucketNumber); - return false; - } - - /** - * Incoming topic message has been forwarded from a remote host. - * - * @param bucketNumber the bucket number calculated on the remote host - * @param keyword the keyword associated with the message - * @param controllerName the controller the message was directed to - * on the remote host - * @param protocol String value of the 'Topic.CommInfrastructure' value - * (UEB, DMAAP, NOOP, or REST -- NOOP and REST shouldn't be used - * here) - * @param topic the UEB/DMAAP topic name - * @param event this is the JSON message - */ - static void topicMessage( - int bucketNumber, String keyword, String controllerName, - String protocol, String topic, String event) { - - // @formatter:off - logger.info("Incoming topic message: Keyword={}, bucket={}\n" - + " controller = {}\n" - + " topic = {}", - keyword, bucketNumber, controllerName, topic); - // @formatter:on - - // locate the 'PolicyController' - PolicyController controller = PolicyControllerConstants.getFactory().get(controllerName); - if (controller == null) { - /* - * This controller existed on the sender's host, but doesn't exist - * on the destination. This is a problem -- we are counting on all - * hosts being configured with the same controllers. - */ - logger.error("Can't locate controller '{}' for incoming topic message", - controllerName); - } else if (controller instanceof TopicListener) { - /* - * This is the destination host -- repeat the 'onTopicEvent' - * method (the one that invoked 'beforeOffer' on the originating host). - * Note that this message could be forwarded again if the sender's - * bucket table was somehow different from ours -- perhaps there was - * an update in progress. - * - * TBD: it would be nice to limit the number of hops, in case we - * somehow have a loop. - */ - ((TopicListener) controller).onTopicEvent( - CommInfrastructure.valueOf(protocol), topic, event); - } else { - /* - * This 'PolicyController' was also a 'TopicListener' on the sender's - * host -- it isn't on this host, and we are counting on them being - * config - */ - logger.error("Controller {} is not a TopicListener", controllerName); - } - } - - /** - * An incoming '/session/insertDrools' message was received. - * - * @param keyword the keyword associated with the incoming object - * @param sessionName encoded session name(groupId:artifactId:droolsSession) - * @param bucket the bucket associated with keyword - * @param ttl similar to IP time-to-live -- it controls the number of hops - * the message may take - * @param data base64-encoded serialized data for the object - */ - static void incomingInsertDrools( - String keyword, String sessionName, int bucket, int ttl, byte[] data) { - - logger.info("Incoming insertDrools: keyword={}, session={}, bucket={}, ttl={}", - keyword, sessionName, bucket, ttl); - - if (Bucket.isKeyOnThisServer(keyword)) { - // process locally - - // [0]="<groupId>" [1]="<artifactId>", [2]="<sessionName>" - String[] nameSegments = sessionName.split(":"); - - // locate the 'PolicyContainer' and 'PolicySession' - PolicySession policySession = locatePolicySession(nameSegments); - - if (policySession == null) { - logger.error("incomingInsertDrools: Can't find PolicySession={}", - sessionName); - } else { - KieSession kieSession = policySession.getKieSession(); - if (kieSession != null) { - try { - // deserialization needs to use the correct class loader - Object obj = Util.deserialize( - Base64.getDecoder().decode(data), - policySession.getPolicyContainer().getClassLoader()); - kieSession.insert(obj); - } catch (IOException | ClassNotFoundException - | IllegalArgumentException e) { - logger.error("incomingInsertDrools: failed to read data " - + "for session '{}'", sessionName, e); - } - } - } - } else { - ttl -= 1; - if (ttl > 0) { - /* - * This host is not the intended destination -- this could happen - * if it was sent from another site. Forward the message in the - * same thread. - */ - forwardInsertDroolsMessage(bucket, keyword, sessionName, ttl, data); - } - } - } - - /** - * step through all 'PolicyContainer' instances looking - * for a matching 'artifactId' & 'groupId'. - * @param nameSegments name portion from sessionName - * @return policySession match artifactId and groupId - */ - private static PolicySession locatePolicySession(String[] nameSegments) { - PolicySession policySession = null; - if (nameSegments.length == 3) { - for (PolicyContainer pc : PolicyContainer.getPolicyContainers()) { - if (nameSegments[1].equals(pc.getArtifactId()) - && nameSegments[0].equals(pc.getGroupId())) { - policySession = pc.getPolicySession(nameSegments[2]); - break; - } - } - } - return policySession; - } - - /** - * Forward the insertDrools message in the same thread. - */ - private static void forwardInsertDroolsMessage(int bucket, String keyword, - String sessionName, int ttl, byte[] data) { - Server server = Bucket.bucketToServer(bucket); - WebTarget webTarget = server.getWebTarget("session/insertDrools"); - if (webTarget != null) { - logger.info("Forwarding 'session/insertDrools' " - + "(key={},session={},bucket={},ttl={})", - keyword, sessionName, bucket, ttl); - Entity<String> entity = - Entity.entity(new String(data, StandardCharsets.UTF_8), - MediaType.APPLICATION_OCTET_STREAM_TYPE); - webTarget - .queryParam(QP_KEYWORD, keyword) - .queryParam(QP_SESSION, sessionName) - .queryParam(QP_BUCKET, bucket) - .queryParam(QP_TTL, ttl) - .request().post(entity); - } - } - - /** - * This method builds the table that is used to locate the appropriate - * keywords within incoming JSON messages (e.g. 'requestID'). The - * associated values are then mapped into bucket numbers. - */ - private static void buildKeywordTable() { - Properties prop = ServerPoolProperties.getProperties(); - - // iterate over all of the properties, picking out those we are - // interested in - for (String name : prop.stringPropertyNames()) { - String topic = null; - String begin; - String end; - - if (name.startsWith(KEYWORD_PROPERTY_START_1) - && name.endsWith(KEYWORD_PROPERTY_END_1)) { - // 1) keyword.<topic>.path=<field-list> - // 2) keyword.path=<field-list> - begin = KEYWORD_PROPERTY_START_1; - end = KEYWORD_PROPERTY_END_1; - } else if (name.startsWith(KEYWORD_PROPERTY_START_2) - && name.endsWith(KEYWORD_PROPERTY_END_2)) { - // 3) ueb.source.topics.<topic>.keyword=<field-list> - // 4) ueb.source.topics.keyword=<field-list> - begin = KEYWORD_PROPERTY_START_2; - end = KEYWORD_PROPERTY_END_2; - } else if (name.startsWith(KEYWORD_PROPERTY_START_3) - && name.endsWith(KEYWORD_PROPERTY_END_3)) { - // 5) dmaap.source.topics.<topic>.keyword=<field-list> - // 6) dmaap.source.topics.keyword=<field-list> - begin = KEYWORD_PROPERTY_START_3; - end = KEYWORD_PROPERTY_END_3; - } else { - // we aren't interested in this property - continue; - } - - topic = detmTopic(name, begin, end); - - // now, process the value - // Example: requestID,CommonHeader.RequestID - String[][] paths = splitPaths(prop, name); - - if (topic == null) { - // these paths are used for any topics not explicitly - // in the 'topicToPaths' table - defaultPaths = paths; - } else { - // these paths are specific to 'topic' - topicToPaths.put(topic, paths); - } - } - } - - private static String detmTopic(String name, String begin, String end) { - int beginIndex = begin.length(); - int endIndex = name.length() - end.length(); - if (beginIndex < endIndex) { - // <topic> is specified, so this table is limited to this - // specific topic - return name.substring(beginIndex, endIndex); - } - - return null; - } - - private static String[][] splitPaths(Properties prop, String name) { - String[] commaSeparatedEntries = prop.getProperty(name).split(","); - String[][] paths = new String[commaSeparatedEntries.length][]; - for (int i = 0; i < commaSeparatedEntries.length; i += 1) { - paths[i] = commaSeparatedEntries[i].split("\\."); - } - - return paths; - } - - /*======================================*/ - /* 'DroolsPdpStateControlApi' interface */ - /*======================================*/ - - /* - * Stop the processing of messages and server pool participation(non-Javadoc) - * Note: This is not static because it should only be used if feature-server-pool - * has been enabled. - * (non-Javadoc) - * @see org.onap.policy.drools.control.api.DroolsPdpStateControlApi#shutdown() - */ - @Override - public void shutdown() { - PolicyEngineConstants.getManager().deactivate(); - Server.shutdown(); - } - - /* - * Stop the processing of messages and server pool participation(non-Javadoc) - * Note: This is not static because it should only be used if feature-server-pool - * has been enabled. - * (non-Javadoc) - * @see org.onap.policy.drools.control.api.DroolsPdpStateControlApi#restart() - */ - @Override - public void restart() { - MainLoop.startThread(); - Discovery.startDiscovery(); - PolicyEngineConstants.getManager().activate(); - } - - /* ============================================================ */ - - /** - * This class implements the 'Bucket.Message' interface for UEB/DMAAP - * messages. - */ - @AllArgsConstructor - private static class TopicMessage implements Bucket.Message { - /* - * the keyword associated with this message - * (which determines the bucket number). - */ - private final String keyword; - - // the controller receiving this message - private final PolicyController controller; - - // enumeration: UEB or DMAAP - private final CommInfrastructure protocol; - - // UEB/DMAAP topic - private final String topic; - - // JSON message as a String - private final String event; - - /** - * Process this message locally using 'TopicListener.onTopicEvent' - * (the 'PolicyController' instance is assumed to implement - * the 'TopicListener' interface as well). - */ - @Override - public void process() { - if (controller instanceof TopicListener) { - /* - * This is the destination host -- repeat the 'onTopicEvent' method - * (the one that invoked 'beforeOffer' on the originating host). - * Note that this message could be forwarded again if the sender's - * bucket table was somehow different from ours -- perhaps there was - * an update in progress. - * - * TBD: it would be nice to limit the number of hops, in case we - * somehow have a loop. - */ - ((TopicListener) controller).onTopicEvent(protocol, topic, event); - } else { - /* - * This 'PolicyController' was also a 'TopicListener' on the sender's - * host -- it isn't on this host, and we are counting on them being - * configured the same way. - */ - logger.error("Controller {} is not a TopicListener", - controller.getName()); - } - } - - /** - * Send this message to a remote server for processing (presumably, it - * is the destination host). - * - * @param server the Server instance to send the message to - * @param bucketNumber the bucket number to send it to - */ - @Override - public void sendToServer(Server server, int bucketNumber) { - // if we reach this point, we have determined the remote server - // that should process this message - - // @formatter:off - logger.info("Outgoing topic message: Keyword={}, bucket={}\n" - + " controller = {}" - + " topic = {}" - + " sender = {}" - + " receiver = {}", - keyword, bucketNumber, controller.getName(), topic, - Server.getThisServer().getUuid(), server.getUuid()); - // @formatter:on - - Entity<String> entity = Entity.entity(event, MediaType.APPLICATION_JSON); - server.post("bucket/topic", entity, new Server.PostResponse() { - @Override - public WebTarget webTarget(WebTarget webTarget) { - return webTarget - .queryParam(QP_BUCKET, bucketNumber) - .queryParam(QP_KEYWORD, keyword) - .queryParam(QP_CONTROLLER, controller.getName()) - .queryParam(QP_PROTOCOL, protocol.toString()) - .queryParam(QP_TOPIC, topic); - } - - @Override - public void response(Response response) { - // log a message indicating success/failure - int status = response.getStatus(); - if (status >= 200 && status <= 299) { - logger.info("/bucket/topic response code = {}", status); - } else { - logger.error("/bucket/topic response code = {}", status); - } - } - }); - } - } - - /* ============================================================ */ - - /** - * Backup data associated with a Drools session. - */ - static class DroolsSessionBackup implements Bucket.Backup { - /** - * {@inheritDoc} - */ - @Override - public Bucket.Restore generate(int bucketNumber) { - // Go through all of the Drools sessions, and generate backup data. - // If there is no data to backup for this bucket, return 'null' - - DroolsSessionRestore restore = new DroolsSessionRestore(); - return restore.backup(bucketNumber) ? restore : null; - } - } - - /* ============================================================ */ - - /** - * This class is used to generate and restore backup Drools data. - */ - static class DroolsSessionRestore implements Bucket.Restore, Serializable { - private static final long serialVersionUID = 1L; - - // backup data for all Drools sessions on this host - private final List<SingleSession> sessions = new LinkedList<>(); - - /** - * {@inheritDoc} - */ - boolean backup(int bucketNumber) { - /* - * There may be multiple Drools sessions being backed up at the same - * time. There is one 'Pair' in the list for each session being - * backed up. - */ - LinkedList<Pair<CompletableFuture<List<Object>>, PolicySession>> - pendingData = new LinkedList<>(); - for (PolicyContainer pc : PolicyContainer.getPolicyContainers()) { - for (PolicySession session : pc.getPolicySessions()) { - // Wraps list of objects, to be populated in the session - final CompletableFuture<List<Object>> droolsObjectsWrapper = - new CompletableFuture<>(); - - // 'KieSessionObject' - final KieSession kieSession = session.getKieSession(); - - logger.info("{}: about to fetch data for session {}", - this, session.getFullName()); - DroolsRunnable backupAndRemove = () -> { - List<Object> droolsObjects = new ArrayList<>(); - for (FactHandle fh : kieSession.getFactHandles()) { - Object obj = kieSession.getObject(fh); - String keyword = Keyword.lookupKeyword(obj); - if (keyword != null - && Bucket.bucketNumber(keyword) == bucketNumber) { - // bucket matches -- include this object - droolsObjects.add(obj); - /* - * delete this factHandle from Drools memory - * this classes are used in bucket migration, - * so the delete is intentional. - */ - kieSession.delete(fh); - } - } - - // send notification that object list is complete - droolsObjectsWrapper.complete(droolsObjects); - }; - kieSession.insert(backupAndRemove); - - // add pending operation to the list - pendingData.add(Pair.of(droolsObjectsWrapper, session)); - } - } - - /* - * data copying can start as soon as we receive results - * from pending sessions (there may not be any) - */ - copyDataFromSession(pendingData); - return !sessions.isEmpty(); - } - - /** - * Copy data from pending sessions. - * @param pendingData a list of policy sessions - */ - private void copyDataFromSession(List<Pair<CompletableFuture<List<Object>>, PolicySession>> - pendingData) { - long endTime = System.currentTimeMillis() + droolsTimeoutMillis; - - for (Pair<CompletableFuture<List<Object>>, PolicySession> pair : - pendingData) { - PolicySession session = pair.getRight(); - long delay = endTime - System.currentTimeMillis(); - if (delay < 0) { - /* - * we have already reached the time limit, so we will - * only process data that has already been received - */ - delay = 0; - } - try { - List<Object> droolsObjects = - pair.getLeft().get(delay, TimeUnit.MILLISECONDS); - - // if we reach this point, session data read has completed - logger.info("{}: session={}, got {} object(s)", - this, session.getFullName(), - droolsObjects.size()); - if (!droolsObjects.isEmpty()) { - sessions.add(new SingleSession(session, droolsObjects)); - } - } catch (TimeoutException e) { - logger.error("{}: Timeout waiting for data from session {}", - this, session.getFullName()); - } catch (Exception e) { - logger.error("{}: Exception writing output data", this, e); - } - } - } - - /** - * {@inheritDoc} - */ - @Override - public void restore(int bucketNumber) { - /* - * There may be multiple Drools sessions being restored at the same - * time. There is one entry in 'sessionLatches' for each session - * being restored. - */ - LinkedList<CountDownLatch> sessionLatches = new LinkedList<>(); - for (SingleSession session : sessions) { - try { - CountDownLatch sessionLatch = session.restore(); - if (sessionLatch != null) { - // there is a restore in progress -- add it to the list - sessionLatches.add(sessionLatch); - } - } catch (IOException | ClassNotFoundException e) { - logger.error("Exception in {}", this, e); - } - } - - // wait for all sessions to be updated - try { - for (CountDownLatch sessionLatch : sessionLatches) { - if (!sessionLatch.await(droolsTimeoutMillis, TimeUnit.MILLISECONDS)) { - logger.error("{}: timed out waiting for session latch", this); - } - } - } catch (InterruptedException e) { - logger.error("Exception in {}", this, e); - Thread.currentThread().interrupt(); - } - } - } - - /* ============================================================ */ - - /** - * Each instance of this class corresponds to a Drools session that has - * been backed up, or is being restored. - */ - static class SingleSession implements Serializable { - private static final long serialVersionUID = 1L; - - // the group id associated with the Drools container - String groupId; - - // the artifact id associated with the Drools container - String artifactId; - - // the session name within the Drools container - String sessionName; - - // serialized data associated with this session (and bucket) - byte[] data; - - /** - * Constructor - initialize the 'SingleSession' instance, so it can - * be serialized. - * - * @param session the Drools session being backed up - * @param droolsObjects the Drools objects from this session associated - * with the bucket currently being backed up - */ - SingleSession(PolicySession session, List<Object> droolsObjects) throws IOException { - // 'groupId' and 'artifactId' are set from the 'PolicyContainer' - PolicyContainer pc = session.getPolicyContainer(); - groupId = pc.getGroupId(); - artifactId = pc.getArtifactId(); - - // 'sessionName' is set from the 'PolicySession' - sessionName = session.getName(); - - /* - * serialize the Drools objects -- we serialize them here, because they - * need to be deserialized within the scope of the Drools session - */ - data = Util.serialize(droolsObjects); - } - - CountDownLatch restore() throws IOException, ClassNotFoundException { - PolicySession session = null; - - // locate the 'PolicyContainer', and 'PolicySession' - for (PolicyContainer pc : PolicyContainer.getPolicyContainers()) { - if (artifactId.equals(pc.getArtifactId()) - && groupId.equals(pc.getGroupId())) { - session = pc.getPolicySession(sessionName); - return insertSessionData(session, new ByteArrayInputStream(data)); - } - } - logger.error("{}: unable to locate session name {}", this, sessionName); - return null; - } - - /** - * Deserialize session data, and insert the objects into the session - * from within the Drools session thread. - * - * @param session the associated PolicySession instance - * @param bis the data to be deserialized - * @return a CountDownLatch, which will indicate when the operation has - * completed (null in case of failure) - * @throws IOException IO errors while creating or reading from - * the object stream - * @throws ClassNotFoundException class not found during deserialization - */ - private CountDownLatch insertSessionData(PolicySession session, ByteArrayInputStream bis) - throws IOException, ClassNotFoundException { - ClassLoader classLoader = session.getPolicyContainer().getClassLoader(); - ExtendedObjectInputStream ois = - new ExtendedObjectInputStream(bis, classLoader); - - /* - * associate the current thread with the session, - * and deserialize - */ - session.setPolicySession(); - Object obj = ois.readObject(); - - if (obj instanceof List) { - final List<?> droolsObjects = (List<?>) obj; - logger.info("{}: session={}, got {} object(s)", - this, session.getFullName(), droolsObjects.size()); - - // signal when session update is complete - final CountDownLatch sessionLatch = new CountDownLatch(1); - - // 'KieSession' object - final KieSession kieSession = session.getKieSession(); - - // run the following within the Drools session thread - DroolsRunnable doRestore = () -> { - try { - /* - * Insert all of the objects -- note that this is running - * in the session thread, so no other rules can fire - * until all of the objects are inserted. - */ - for (Object droolsObj : droolsObjects) { - kieSession.insert(droolsObj); - } - } finally { - // send notification that the inserts have completed - sessionLatch.countDown(); - } - }; - kieSession.insert(doRestore); - ois.close(); - return sessionLatch; - } else { - ois.close(); - logger.error("{}: Invalid session data for session={}, type={}", - this, session.getFullName(), obj.getClass().getName()); - } - return null; - } - } -} diff --git a/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/Keyword.java b/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/Keyword.java deleted file mode 100644 index 0059a4b9..00000000 --- a/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/Keyword.java +++ /dev/null @@ -1,500 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * feature-server-pool - * ================================================================================ - * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved. - * ================================================================================ - * 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. - * ============LICENSE_END========================================================= - */ - -package org.onap.policy.drools.serverpool; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.HashMap; -import java.util.Map; -import java.util.Properties; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.UnaryOperator; -import lombok.AllArgsConstructor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * This class supports the lookup of keywords from objects within Drools - * sessions. It maps the class of the object into an object implementing - * the 'Keyword.Lookup' interface. At present, this requires writing - * special code for each class that can exist in a Drools session that is - * assignable and relocatable via a bucket. In theory, it would be possible - * to populate this table through properties, which would use the reflective - * interface, and indicate the methods and fields to use to do this lookup. - */ -public class Keyword { - private static Logger logger = LoggerFactory.getLogger(Keyword.class); - - // this table can be used to map an object class into the method - // to invoke to do the lookup - private static ConcurrentHashMap<Class<?>, Lookup> classToLookup = - new ConcurrentHashMap<>(); - - // this is a pre-defined 'Lookup' instance that always returns 'null' - private static Lookup nullLookup = (Object obj) -> null; - - /** - * This method takes the object's class, looks it up in the 'classToLookup' - * table, and then performs the lookup to get the keyword. When a direct - * lookup on a class fails, it will attempt to find a match using inheritance - * rules -- if an appropriate match is found, the 'classToLookup' table is - * updated, so it will be easier next time. If no match is found, the table - * is also updated, but the 'value' will be 'nullLookup'. - * - * @param obj object to to the lookup on - * @return a String keyword, if found; 'null' if not - */ - public static String lookupKeyword(Object obj) { - Lookup lu = classToLookup.get(obj.getClass()); - if (lu != null) { - return lu.getKeyword(obj); - } - // no entry for this class yet -- - // try to locate a matching entry using 'inheritance' rules - Class<?> thisClass = obj.getClass(); - Class<?> matchingClass = null; - for (Map.Entry<Class<?>, Lookup> entry : classToLookup.entrySet()) { - if (entry.getKey().isAssignableFrom(thisClass) - && (matchingClass == null - || matchingClass.isAssignableFrom(entry.getKey()))) { - // we have a first match, or a more specific match - matchingClass = entry.getKey(); - lu = entry.getValue(); - } - } - - /* - * whether we found a match or not, update the table accordingly - * no match found -- see if the 'keyword.<CLASS-NAME>.lookup' - * properties can provide a solution. - */ - if (lu == null && (lu = buildReflectiveLookup(thisClass)) == null) { - lu = nullLookup; - } - - // update table - classToLookup.put(thisClass, lu); - return lu.getKeyword(obj); - } - - /** - * explicitly place an entry in the table. - * - * @param clazz the class to do the lookup on - * @param handler an instance implementing the 'Lookup' interface, - * can handle instances of type 'clazz' - */ - public static void setLookupHandler(Class<?> clazz, Lookup handler) { - classToLookup.put(clazz, handler); - } - - /* ============================================================ */ - - /** - * These are the interface that must be implemented by objects in the - * 'classToLookup' table. - */ - public interface Lookup { - /** - * Map the object into a keyword string. - * - * @param obj the object to lookup, which should be an instance of the - * associated class in the 'classToLookup' table - * @return the keyword, if found; 'null' if not - */ - public String getKeyword(Object obj); - } - - /* ============================================================ */ - - // this table maps class name to a sequence of method calls and field - // references, based upon 'keyword.<CLASS-NAME>.lookup' entries found - // in the property list - private static Map<String, String> classNameToSequence = null; - - static final String KEYWORD_PROPERTY_START = "keyword."; - static final String KEYWORD_PROPERTY_END = ".lookup"; - - /** - * Attempt to build a 'Lookup' instance for a particular class based upon - * properties. - * - * @param clazz the class to build an entry for - * @return a 'Lookup' instance to do the lookup, or 'null' if one can't - * be generated from the available properties - */ - private static synchronized Lookup buildReflectiveLookup(Class<?> clazz) { - if (classNameToSequence == null) { - classNameToSequence = new HashMap<>(); - Properties prop = ServerPoolProperties.getProperties(); - - /* - * iterate over all of the properties, picking out those - * that match the name 'keyword.<CLASS-NAME>.lookup' - */ - for (String name : prop.stringPropertyNames()) { - if (name.startsWith(KEYWORD_PROPERTY_START) - && name.endsWith(KEYWORD_PROPERTY_END)) { - // this property matches -- locate the '<CLASS-NAME>' part - int beginIndex = KEYWORD_PROPERTY_START.length(); - int endIndex = name.length() - - KEYWORD_PROPERTY_END.length(); - if (beginIndex < endIndex) { - // add it to the table - classNameToSequence.put(name.substring(beginIndex, endIndex), - prop.getProperty(name)); - } - } - } - } - - Class<?> keyClass = buildReflectiveLookupFindKeyClass(clazz); - - if (keyClass == null) { - // no matching class name found - return null; - } - - return buildReflectiveLookupBuild(clazz, keyClass); - } - - /** - * Look for the "best match" for class 'clazz' in the hash table. - * First, look for the name of 'clazz' itself, followed by all of - * interfaces. If no match is found, repeat with the superclass, - * and all the way up the superclass chain. - */ - private static Class<?> buildReflectiveLookupFindKeyClass(Class<?> clazz) { - for (Class<?> cl = clazz; cl != null; cl = cl.getSuperclass()) { - if (classNameToSequence.containsKey(cl.getName())) { - // matches the class - return cl; - } - for (Class<?> intf : cl.getInterfaces()) { - if (classNameToSequence.containsKey(intf.getName())) { - // matches one of the interfaces - return intf; - } - // interface can have superclass - for (Class<?> cla = clazz; cla != null; cla = intf.getSuperclass()) { - if (classNameToSequence.containsKey(cla.getName())) { - // matches the class - return cla; - } - } - } - } - return null; - } - - private static Lookup buildReflectiveLookupBuild(Class<?> clazz, Class<?> keyClass) { - // we found a matching key in the table -- now, process the values - Class<?> currentClass = keyClass; - - /* - * there may potentially be a chain of entries if multiple - * field and/or method calls are in the sequence -- this is the first - */ - ReflectiveLookup first = null; - - // this is the last entry in the list - ReflectiveLookup last = null; - - /* - * split the value into segments, where each segment has the form - * 'FIELD-NAME' or 'METHOD-NAME()', with an optional ':CONVERSION' - * at the end - */ - String sequence = classNameToSequence.get(keyClass.getName()); - ConversionFunctionLookup conversionFunctionLookup = null; - int index = sequence.indexOf(':'); - if (index >= 0) { - // conversion function specified - conversionFunctionLookup = - new ConversionFunctionLookup(sequence.substring(index + 1)); - sequence = sequence.substring(0, index); - } - for (String segment : sequence.split("\\.")) { - ReflectiveLookup current = null; - ReflectiveOperationException error = null; - try { - if (segment.endsWith("()")) { - // this segment is a method lookup - current = new MethodLookup(currentClass, - segment.substring(0, segment.length() - 2)); - } else { - // this segment is a field lookup - current = new FieldLookup(currentClass, segment); - } - } catch (ReflectiveOperationException e) { - // presumably the field or method does not exist in this class - error = e; - } - if (current == null) { - logger.error("Keyword.buildReflectiveLookup: build error " - + "(class={},value={},segment={})", - clazz.getName(), - classNameToSequence.get(keyClass.getName()), - segment, - error); - return null; - } - - // if we reach this point, we processed this segment successfully - currentClass = current.nextClass(); - if (first == null) { - // the initial segment - first = current; - } else { - // link to the most recently created segment - last.next = current; - } - // update most recently created segment - last = current; - } - - // add optional conversion function ('null' if it doesn't exist) - // The preceding 'sequence.split(...)' will always return at - // least one entry, so there will be at least one - // attempt to go through the loop. If it then makes it out of the loop - // without returning (or an exception), 'current' and 'last' will both be - // set to non-null values. - last.next = conversionFunctionLookup; // NOSONAR - - // successful - return the first 'Lookup' instance in the chain - return first; - } - - /* ============================================================ */ - - /** - * Abstract superclass of 'FieldLookup' and 'MethodLookup'. - */ - private abstract static class ReflectiveLookup implements Lookup { - // link to the next 'Lookup' instance in the chain - Lookup next = null; - - /** - * Return the next 'class' instance. - * - * @return the class associated with the return value of the - * field or method lookup - */ - abstract Class<?> nextClass(); - } - - /* ============================================================ */ - - /** - * This class is used to do a field lookup. - */ - private static class FieldLookup extends ReflectiveLookup { - // the reflective 'Field' instance associated with this lookup - Field field; - - /** - * Constructor. - * - * @param clazz the 'class' we are doing the field lookup on - * @param segment a segment from the property value, which is just the - * field name - */ - FieldLookup(Class<?> clazz, String segment) throws NoSuchFieldException { - field = clazz.getField(segment); - } - - /* ****************************** */ - /* 'ReflectiveLookup' interface */ - /* ****************************** */ - - /** - * {@inheritDoc} - */ - @Override - Class<?> nextClass() { - return field.getType(); - } - - /* ******************** */ - /* 'Lookup' interface */ - /* ******************** */ - - /** - * {@inheritDoc} - */ - @Override - public String getKeyword(Object obj) { - try { - // do the field lookup - Object rval = field.get(obj); - if (rval == null) { - return null; - } - - // If there is no 'next' entry specified, this value is the - // keyword. Otherwise, move on to the next 'Lookup' entry in - // the chain. - return next == null ? rval.toString() : next.getKeyword(rval); - } catch (Exception e) { - logger.error("Keyword.FieldLookup error: field={}", - field, e); - return null; - } - } - } - - /* ============================================================ */ - - /** - * This class is used to do a method call on the target object. - */ - private static class MethodLookup extends ReflectiveLookup { - // the reflective 'Method' instance associated with this lookup - Method method; - - /** - * Constructor. - * - * @param clazz the 'class' we are doing the method lookup on - * @param name a method name extracted from a segment from the - * property value, which is the - */ - MethodLookup(Class<?> clazz, String name) throws NoSuchMethodException { - method = clazz.getMethod(name); - } - - /*==============================*/ - /* 'ReflectiveLookup' interface */ - /*==============================*/ - - /** - * {@inheritDoc} - */ - @Override - Class<?> nextClass() { - return method.getReturnType(); - } - - /*====================*/ - /* 'Lookup' interface */ - /*====================*/ - - /** - * {@inheritDoc} - */ - @Override - public String getKeyword(Object obj) { - try { - // do the method call - Object rval = method.invoke(obj); - if (rval == null) { - return null; - } - - // If there is no 'next' entry specified, this value is the - // keyword. Otherwise, move on to the next 'Lookup' entry in - // the chain. - return next == null ? rval.toString() : next.getKeyword(rval); - } catch (Exception e) { - logger.error("Keyword.MethodLookup error: method={}", - method, e); - return null; - } - } - } - - /* ============================================================ */ - - /* - * Support for named "conversion functions", which take an input keyword, - * and return a possibly different keyword derived from it. The initial - * need is to take a string which consists of a UUID and a suffix, and - * return the base UUID. - */ - - // used to lookup optional conversion functions - private static Map<String, UnaryOperator<String>> conversionFunction = - new ConcurrentHashMap<>(); - - // conversion function 'uuid': - // truncate strings to 36 characters(uuid length) - static final int UUID_LENGTH = 36; - - static { - conversionFunction.put("uuid", value -> - // truncate strings to 36 characters - value != null && value.length() > UUID_LENGTH - ? value.substring(0, UUID_LENGTH) : value - ); - } - - /** - * Add a conversion function. - * - * @param name the conversion function name - * @param function the object that does the transformation - */ - public static void addConversionFunction(String name, UnaryOperator<String> function) { - conversionFunction.put(name, function); - } - - /** - * Apply a named conversion function to a keyword. - * - * @param inputKeyword this is the keyword extracted from a message or object - * @param functionName this is the name of the conversion function to apply - * (if 'null', no conversion is done) - * @return the converted keyword - */ - public static String convertKeyword(String inputKeyword, String functionName) { - if (functionName == null || inputKeyword == null) { - // don't do any conversion -- just return the input keyword - return inputKeyword; - } - - // look up the function - UnaryOperator<String> function = conversionFunction.get(functionName); - if (function == null) { - logger.error("{}: conversion function not found", functionName); - return null; - } - - // call the conversion function, and return the value - return function.apply(inputKeyword); - } - - /** - * This class is used to invoke a conversion function. - */ - @AllArgsConstructor - private static class ConversionFunctionLookup implements Lookup { - // the conversion function name - private final String functionName; - - /** - * {@inheritDoc} - */ - @Override - public String getKeyword(Object obj) { - return obj == null ? null : convertKeyword(obj.toString(), functionName); - } - } -} diff --git a/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/Leader.java b/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/Leader.java deleted file mode 100644 index be291708..00000000 --- a/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/Leader.java +++ /dev/null @@ -1,594 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * feature-server-pool - * ================================================================================ - * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved. - * ================================================================================ - * 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. - * ============LICENSE_END========================================================= - */ - -package org.onap.policy.drools.serverpool; - -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DEFAULT_LEADER_STABLE_IDLE_CYCLES; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DEFAULT_LEADER_STABLE_VOTING_CYCLES; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.LEADER_STABLE_IDLE_CYCLES; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.LEADER_STABLE_VOTING_CYCLES; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.getProperty; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.io.PrintStream; -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.HashSet; -import java.util.TreeMap; -import java.util.TreeSet; -import java.util.UUID; -import javax.ws.rs.client.Entity; -import javax.ws.rs.core.MediaType; -import lombok.EqualsAndHashCode; -import lombok.Setter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * This class handles the election of the lead server. The lead server - * handles bucket assignments, and also is the server running the - * 'Discovery' procedure long-term (other servers do run the procedure - * until a leader is elected). - * Note that everything in this class is run under the 'MainLoop' thread, - * with the exception of the invocation and first two statements of the - * 'voteData' method. - */ -class Leader { - private static Logger logger = LoggerFactory.getLogger(Leader.class); - - // Listener class to handle state changes that may lead to a new election - private static EventHandler eventHandler = new EventHandler(); - - static { - Events.register(eventHandler); - } - - // Server currently in the leader roll - @Setter - private static Server leaderLocal = null; - - // Vote state machine -- it is null, unless a vote is in progress - @Setter - private static VoteCycle voteCycle = null; - - private static UUID emptyUUID = new UUID(0L, 0L); - - /*==================================================*/ - /* Some properties extracted at initialization time */ - /*==================================================*/ - - // how many cycles of stability before voting starts - private static int stableIdleCycles; - - // how may cycles of stability before declaring a winner - private static int stableVotingCycles; - - /** - * Hide implicit public constructor. - */ - private Leader() { - // everything here is static -- no instances of this class are created - } - - /** - * Invoked at startup, or after some events -- immediately start a new vote. - */ - static void startup() { - // fetch some static properties - stableIdleCycles = getProperty(LEADER_STABLE_IDLE_CYCLES, - DEFAULT_LEADER_STABLE_IDLE_CYCLES); - stableVotingCycles = getProperty(LEADER_STABLE_VOTING_CYCLES, - DEFAULT_LEADER_STABLE_VOTING_CYCLES); - - startVoting(); - } - - /** - * start, or restart voting. - */ - private static void startVoting() { - if (voteCycle == null) { - voteCycle = new VoteCycle(); - MainLoop.addBackgroundWork(voteCycle); - } else { - voteCycle.serverChanged(); - } - } - - /** - * Return the current leader. - * - * @return the current leader ('null' if none has been selected) - */ - public static Server getLeader() { - return leaderLocal; - } - - /** - * Handle an incoming /vote REST message. - * - * @param data base64-encoded data, containing vote data - */ - static void voteData(byte[] data) { - // decode base64 data - final byte[] packet = Base64.getDecoder().decode(data); - - MainLoop.queueWork(() -> { - // This runs within the 'MainLoop' thread -- - // create the 'VoteCycle' state machine, if needed - if (voteCycle == null) { - voteCycle = new VoteCycle(); - MainLoop.addBackgroundWork(voteCycle); - } - try { - // pass data to 'VoteCycle' state machine - voteCycle.packetReceived(packet); - } catch (IOException e) { - logger.error("Exception in 'Leader.voteData", e); - } - }); - } - - /* ============================================================ */ - - /** - * There is a single instance of this class (Leader.eventHandler), which - * is registered to listen for notifications of state transitions. Note - * that all of these methods are running within the 'MainLoop' thread. - */ - private static class EventHandler extends Events { - /** - * {@inheritDoc} - */ - @Override - public void newServer(Server server) { - // a new server has joined -- start/restart the VoteCycle state machine - startVoting(); - } - - /** - * {@inheritDoc} - */ - @Override - public void serverFailed(Server server) { - if (server == leaderLocal) { - // the lead server has failed -- - // start/restart the VoteCycle state machine - setLeaderLocal(null); - startVoting(); - - // send out a notification that the lead server has failed - for (Events listener : Events.getListeners()) { - listener.leaderFailed(server); - } - } else if (voteCycle != null) { - // a vote is in progress -- restart the state machine - // (don't do anything if there is no vote in progress) - voteCycle.serverChanged(); - } - } - } - - /* ============================================================ */ - - /** - * This is the 'VoteCycle' state machine -- it runs as background work - * on the 'MainLoop' thread, and goes away when a leader is elected. - */ - private static class VoteCycle implements Runnable { - enum State { - // server just started up -- 5 second grace period - STARTUP, - - // voting in progress -- changes have occurred in the last cycle - VOTING, - } - - // maps UUID voted for into the associated data - private final TreeMap<UUID, VoteData> uuidToVoteData = - new TreeMap<>(Util.uuidComparator); - - // maps voter UUID into the associated data - private final TreeMap<UUID, VoterData> uuidToVoterData = - new TreeMap<>(Util.uuidComparator); - - // sorted list of 'VoteData' (most preferable to least) - private final TreeSet<VoteData> voteData = new TreeSet<>(); - - // data to send out next cycle - private final HashSet<VoterData> updatedVotes = new HashSet<>(); - - private State state = State.STARTUP; - private int cycleCount = stableIdleCycles; - - /** - * Constructor - if there is no leader, or this server is the leader, - * start the 'Discovery' thread. - */ - VoteCycle() { - if (leaderLocal == null || leaderLocal == Server.getThisServer()) { - Discovery.startDiscovery(); - } - } - - /** - * A state change has occurred that invalidates any vote in progress -- - * restart the VoteCycle state machine. - */ - void serverChanged() { - // clear all of the tables - uuidToVoteData.clear(); - uuidToVoterData.clear(); - voteData.clear(); - updatedVotes.clear(); - - // wait for things to stabilize before continuing - state = State.STARTUP; - cycleCount = stableIdleCycles; - } - - /** - * {@inheritDoc} - */ - @Override - public void run() { - switch (state) { - case STARTUP: - startupState(); - break; - - case VOTING: - votingState(); - break; - - default: - logger.error("Unknown state: {}", state); - break; - } - } - - private void startupState() { - // 5-second grace period -- wait for things to stablize before - // starting the vote - cycleCount -= 1; - if (cycleCount <= 0) { - logger.info("VoteCycle: {} seconds have passed", - stableIdleCycles); - updateMyVote(); - sendOutUpdates(); - state = State.VOTING; - cycleCount = stableVotingCycles; - } - } - - private void votingState() { - // need to be in the VOTING state without any vote changes - // for 5 seconds -- once this happens, the leader is chosen - if (sendOutUpdates()) { - // changes have occurred -- set the grace period to 5 seconds - cycleCount = stableVotingCycles; - return; - } - - cycleCount -= 1; - if (cycleCount > 0) { - return; - } - - // 5 second grace period has passed -- the leader is one with - // the most votes, which is the first entry in 'voteData' - Server oldLeader = leaderLocal; - setLeaderLocal(Server.getServer(voteData.first().uuid)); - if (leaderLocal != oldLeader) { - // the leader has changed -- send out notifications - for (Events listener : Events.getListeners()) { - listener.newLeader(leaderLocal); - } - } else { - // the election is over, and the leader has been confirmed - for (Events listener : Events.getListeners()) { - listener.leaderConfirmed(leaderLocal); - } - } - if (leaderLocal == Server.getThisServer()) { - // this is the lead server -- - // make sure the 'Discovery' threads are running - Discovery.startDiscovery(); - } else { - // this is not the lead server -- stop 'Discovery' threads - Discovery.stopDiscovery(); - } - - // we are done with voting -- clean up, and report results - MainLoop.removeBackgroundWork(this); - setVoteCycle(null); - - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - PrintStream out = new PrintStream(bos); - - out.println("Voting results:"); - - // x(36) xxxxx x(36) - // UUID Votes Voter - String format = "%-36s %5s %-36s\n"; - out.format(format, "UUID", "Votes", "Voter(s)"); - out.format(format, "----", "-----", "--------"); - - outputVotes(out, format); - logger.info("Output - {}", bos); - } - - private void outputVotes(PrintStream out, String format) { - for (VoteData vote : voteData) { - if (vote.voters.isEmpty()) { - out.format(format, vote.uuid, 0, ""); - continue; - } - boolean headerNeeded = true; - for (VoterData voter : vote.voters) { - if (headerNeeded) { - out.format(format, vote.uuid, - vote.voters.size(), voter.uuid); - headerNeeded = false; - } else { - out.format(format, "", "", voter.uuid); - } - } - } - } - - /** - * Process an incoming /vote REST message. - * - * @param packet vote data, containing one or more votes - */ - private void packetReceived(byte[] packet) throws IOException { - DataInputStream dis = - new DataInputStream(new ByteArrayInputStream(packet)); - - while (dis.available() != 0) { - // message is a series of: - // 16-bytes voter UUID - // 16-bytes vote UUID - // 8-bytes timestamp - long tmp = dis.readLong(); // most significant bits - UUID voter = new UUID(tmp, dis.readLong()); - - tmp = dis.readLong(); - UUID vote = new UUID(tmp, dis.readLong()); - - long timestamp = dis.readLong(); - - // process the single vote - processVote(voter, vote, timestamp); - } - } - - /** - * Process a single incoming vote. - * - * @param UUID voter the UUID of the Server making this vote - * @param UUID vote the UUID of the Server that 'voter' voted for - * @param timestamp the time when the vote was made - */ - private void processVote(UUID voter, UUID vote, long timestamp) { - // fetch old data for this voter - VoterData voterData = uuidToVoterData.computeIfAbsent(voter, - key -> new VoterData(voter, timestamp)); - if (timestamp >= voterData.timestamp) { - // this is a new vote for this voter -- update the timestamp - voterData.timestamp = timestamp; - } else { - // already processed vote, and it may even be obsolete - return; - } - - // fetch the old vote, if any, for this voter - VoteData oldVoteData = voterData.vote; - VoteData newVoteData = null; - - if (vote != null) { - newVoteData = uuidToVoteData.computeIfAbsent(vote, key -> new VoteData(vote)); - } - - if (oldVoteData != newVoteData) { - // the vote has changed -- update the 'voterData' entry, - // and include this in the next set of outgoing messages - logger.info("{} voting for {}", voter, vote); - voterData.vote = newVoteData; - updatedVotes.add(voterData); - - if (oldVoteData != null) { - // remove the old vote data - voteData.remove(oldVoteData); - oldVoteData.voters.remove(voterData); - if (oldVoteData.voters.isEmpty()) { - // no voters left -- remove the entry - uuidToVoteData.remove(oldVoteData.uuid); - } else { - // reinsert in a new position - voteData.add(oldVoteData); - } - } - - if (newVoteData != null) { - // update the new vote data - voteData.remove(newVoteData); - newVoteData.voters.add(voterData); - voteData.add(newVoteData); - } - } - } - - /** - * If any updates have occurred, send then out to all servers on - * the "notify list". - * - * @return 'true' if one or more votes have changed, 'false' if not - */ - private boolean sendOutUpdates() { - try { - if (updatedVotes.isEmpty()) { - // no changes to send out - return false; - } - - // possibly change vote based on current information - updateMyVote(); - - // generate message to send out - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - DataOutputStream dos = new DataOutputStream(bos); - - // go through all of the updated votes - for (VoterData voterData : updatedVotes) { - // voter UUID - dos.writeLong(voterData.uuid.getMostSignificantBits()); - dos.writeLong(voterData.uuid.getLeastSignificantBits()); - - // vote UUID - UUID vote = - (voterData.vote == null ? emptyUUID : voterData.vote.uuid); - dos.writeLong(vote.getMostSignificantBits()); - dos.writeLong(vote.getLeastSignificantBits()); - - // timestamp - dos.writeLong(voterData.timestamp); - } - updatedVotes.clear(); - - // create an 'Entity' that can be sent out to all hosts - Entity<String> entity = Entity.entity( - new String(Base64.getEncoder().encode(bos.toByteArray()), StandardCharsets.UTF_8), - MediaType.APPLICATION_OCTET_STREAM_TYPE); - - // send out to all servers on the notify list - for (Server server : Server.getNotifyList()) { - server.post("vote", entity); - } - return true; - } catch (IOException e) { - logger.error("Exception in VoteCycle.sendOutUpdates", e); - return false; - } - } - - /** - * (Possibly) change this servers vote, based upon votes of other voters. - */ - private void updateMyVote() { - UUID myVote = null; - - if (uuidToVoterData.size() * 2 < Server.getServerCount()) { - // fewer than half of the nodes have voted - if (leaderLocal != null) { - // choose the current leader - myVote = leaderLocal.getUuid(); - } else { - // choose the first entry in the servers list - myVote = Server.getFirstServer().getUuid(); - } - } else { - // choose the first entry we know about - for (VoteData vote : voteData) { - if (Server.getServer(vote.uuid) != null) { - myVote = vote.uuid; - break; - } - } - } - if (myVote != null) { - // update the vote for this host, and include it in the list - processVote(Server.getThisServer().getUuid(), myVote, - System.currentTimeMillis()); - } - } - } - - /* ============================================================ */ - - /** - * This class corresponds to a single vote recipient -- - * the Server being voted for. - */ - @EqualsAndHashCode - private static class VoteData implements Comparable<VoteData> { - // uuid voted for - private UUID uuid; - - // the set of all voters that voted for this server - private HashSet<VoterData> voters = new HashSet<>(); - - /** - * Constructor -- set the UUID. - */ - VoteData(UUID uuid) { - this.uuid = uuid; - } - - /*================================*/ - /* Comparable<VoteData> interface */ - /*================================*/ - - /** - * {@inheritDoc} - */ - @Override - public int compareTo(VoteData other) { - // favor highest vote count - // in case of a tie, compare UUIDs (favor smallest) - - int rval = other.voters.size() - voters.size(); - if (rval == 0) { - // vote counts equal -- favor the smaller UUID - rval = Util.uuidComparator.compare(uuid, other.uuid); - } - return rval; - } - } - - /* ============================================================ */ - - /** - * This class corresponds to the vote of a single server. - */ - private static class VoterData { - // voter UUID - private UUID uuid; - - // most recently cast vote from this voter - private VoteData vote = null; - - // time when the vote was cast - private long timestamp = 0; - - /** - * Constructor - store the UUID and timestamp. - */ - private VoterData(UUID uuid, long timestamp) { - this.uuid = uuid; - this.timestamp = timestamp; - } - } -} diff --git a/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/MainLoop.java b/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/MainLoop.java deleted file mode 100644 index ca5e86ac..00000000 --- a/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/MainLoop.java +++ /dev/null @@ -1,195 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * feature-server-pool - * ================================================================================ - * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved. - * ================================================================================ - * 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. - * ============LICENSE_END========================================================= - */ - -package org.onap.policy.drools.serverpool; - -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DEFAULT_MAINLOOP_CYCLE; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.MAINLOOP_CYCLE; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.getProperty; - -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.LinkedTransferQueue; -import java.util.concurrent.TimeUnit; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * This class provides a single thread that is used for 'Server' and 'Bucket' - * updates. This simplifies things because it greatly reduces the need for - * synchronization within these classes. - */ -class MainLoop extends Thread { - private static Logger logger = LoggerFactory.getLogger(MainLoop.class); - - // this queue is used to send work to the 'MainLoop' thread, for things - // like processing incoming messages - private static LinkedTransferQueue<Runnable> incomingWork = - new LinkedTransferQueue<>(); - - // this is used for work that should be invoked every cycle - private static ConcurrentLinkedQueue<Runnable> backgroundWork = - new ConcurrentLinkedQueue<>(); - - // this is the 'MainLoop' thread - private static volatile MainLoop mainLoop = null; - - // main loop cycle time - private static long cycleTime; - - /** - * If it isn't already running, start the 'MainLoop' thread. - */ - public static synchronized void startThread() { - cycleTime = getProperty(MAINLOOP_CYCLE, DEFAULT_MAINLOOP_CYCLE); - if (mainLoop == null) { - mainLoop = new MainLoop(); - mainLoop.start(); - } - } - - /** - * If it is currently running, stop the 'MainLoop' thread. - */ - public static synchronized void stopThread() { - // this won't be immediate, but the thread should discover it shortly - MainLoop saveMainLoop = mainLoop; - - mainLoop = null; - if (saveMainLoop != null) { - saveMainLoop.interrupt(); - } - } - - /** - * Add some work to the 'incomingWork' queue -- this runs once, and is - * automatically removed from the queue. - * - * @param work this is the Runnable to invoke - */ - public static void queueWork(Runnable work) { - if (!incomingWork.offer(work)) { - logger.info("incomingWork returned false"); - } - } - - /** - * Add some work to the 'backgroundWork' queue -- this runs every cycle, - * until it is manually removed. - * - * @param work this is the Runnable to invoke every cycle - */ - public static void addBackgroundWork(Runnable work) { - // if it is already here, remove it first - backgroundWork.remove(work); - - // add to the end of the queue - backgroundWork.add(work); - } - - /** - * Remove work from the 'backgroundWork' queue. - * - * @param work this is the Runnable to remove from the queue - * @return true if the background work was found, and removed - */ - public static boolean removeBackgroundWork(Runnable work) { - return backgroundWork.remove(work); - } - - /** - * Constructor. - */ - private MainLoop() { - super("Main Administrative Loop"); - } - - /** - * This is the main processing loop for "administrative" messages, which - * manage 'Server' states. - * 1) Process incoming messages (other threads are reading in and queueing - * the messages), making note of information that should forwarded to - * other servers. - * 2) Send out updates to all servers on the 'notify' list - * 3) Go through list of all 'Server' entries, and see which ones have - * taken too long to respond -- those are treated as 'failed' - */ - @Override - public void run() { - while (this == mainLoop) { - try { - // the following reads in messages over a period of 1 second - handleIncomingWork(); - - // send out notifications to other hosts - Server.sendOutData(); - - // search for hosts which have taken too long to respond - Server.searchForFailedServers(); - - // work that runs every cycle - for (Runnable work : backgroundWork) { - backgroundWorkRunnable(work); - } - } catch (Exception e) { - logger.error("Exception in MainLoop", e); - } - } - } - - /** - * Runnable try loop. - */ - static void backgroundWorkRunnable(Runnable work) { - try { - work.run(); - } catch (Exception e) { - logger.error("Exception in MainLoop background work", e); - } - } - - /** - * Poll for and process incoming messages for up to 1 second. - */ - static void handleIncomingWork() { - long currentTime = System.currentTimeMillis(); - long wakeUpTime = currentTime + cycleTime; - long timeDiff; - - // receive incoming messages - while ((timeDiff = wakeUpTime - currentTime) > 0) { - try { - Runnable work = - incomingWork.poll(timeDiff, TimeUnit.MILLISECONDS); - if (work == null) { - // timeout -- we are done processing messages for now - return; - } - work.run(); - } catch (InterruptedException e) { - logger.error("Interrupted in MainLoop"); - Thread.currentThread().interrupt(); - return; - } catch (Exception e) { - logger.error("Exception in MainLoop incoming work", e); - } - currentTime = System.currentTimeMillis(); - } - } -} diff --git a/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/RestServerPool.java b/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/RestServerPool.java deleted file mode 100644 index 2c0a2544..00000000 --- a/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/RestServerPool.java +++ /dev/null @@ -1,437 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * feature-server-pool - * ================================================================================ - * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved. - * ================================================================================ - * 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. - * ============LICENSE_END========================================================= - */ - -package org.onap.policy.drools.serverpool; - -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.Info; -import io.swagger.annotations.SwaggerDefinition; -import io.swagger.annotations.Tag; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.PrintStream; -import java.nio.charset.StandardCharsets; -import java.util.UUID; -import javax.ws.rs.Consumes; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * This class contains methods for processing incoming REST messages. - */ - -@Path("/") -@Api -@SwaggerDefinition( - info = @Info( - description = "PDP-D Server Pool Telemetry Service", - version = "v1.0", - title = "PDP-D Server Pool Telemetry" - ), - consumes = {MediaType.APPLICATION_JSON}, - produces = {MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}, - schemes = {SwaggerDefinition.Scheme.HTTP}, - tags = { - @Tag(name = "pdp-d-server-pool-telemetry", description = "Drools PDP Server Pool Telemetry Operations") - } - ) -public class RestServerPool { - private static Logger logger = LoggerFactory.getLogger(RestServerPool.class); - - /** - * Handle the '/test' REST call. - */ - @GET - @Path("/test") - @ApiOperation( - value = "Perform an incoming /test request", - notes = "Provides an acknowledge message back to requestor", - response = String.class - ) - @Produces(MediaType.TEXT_PLAIN) - public String test() { - return "RestServerPool.test()"; - } - - /* ============================================================ */ - - /** - * Handle the '/admin' REST call. - */ - @POST - @Path("/admin") - @ApiOperation( - value = "Perform an incoming /admin request", - notes = "This rest call decodes incoming admin message (base-64-encoded) and " - + "send to main thread for processing" - ) - @Consumes(MediaType.APPLICATION_OCTET_STREAM) - public void adminRequest(byte[] data) { - Server.adminRequest(data); - } - - /** - * Handle the '/vote' REST call. - */ - @POST - @Path("/vote") - @ApiOperation( - value = "Perform an incoming /vote request", - notes = "The request data containing voter and vote data to be processed" - ) - @Consumes(MediaType.APPLICATION_OCTET_STREAM) - public void voteData(byte[] data) { - Leader.voteData(data); - } - - /** - * Handle the '/bucket/update' REST call. - */ - @POST - @Path("/bucket/update") - @ApiOperation( - value = "Perform an incoming /bucket/update request", - notes = "The request data include owner, state, primaryBackup and secondaryBackup" - ) - @Consumes(MediaType.APPLICATION_OCTET_STREAM) - public void updateBucket(byte[] data) { - Bucket.updateBucket(data); - } - - /** - * Handle the '/bucket/topic' REST call. - */ - @POST - @Path("/bucket/topic") - @ApiOperation( - value = "Perform an incoming /bucket/topic request", - notes = "Forward an incoming topic message from a remote host, the request data include " - + "bucketNumber the bucket number calculated on the remote host, keyword the keyword " - + "associated with the message, controllerName the controller the message was directed to " - + "on the remote host, protocol String value of the topic value (UEB, DMAAP, NOOP, or REST " - + "-- NOOP and REST shouldn't be used here), topic the UEB/DMAAP topic name, event this is " - + "the JSON message" - ) - @Consumes(MediaType.APPLICATION_JSON) - public void topicMessage(@QueryParam("bucket") Integer bucket, - @QueryParam("keyword") String keyword, - @QueryParam("controller") String controllerName, - @QueryParam("protocol") String protocol, - @QueryParam("topic") String topic, - String event) { - FeatureServerPool.topicMessage(bucket, keyword, controllerName, protocol, topic, event); - } - - /** - * Handle the '/bucket/sessionData' REST call. - */ - @POST - @Path("/bucket/sessionData") - @ApiOperation( - value = "Perform an incoming /bucket/sessionData request", - notes = "A message is received from the old owner of the bucket and send to new owner, " - + "the request data include bucketNumber the bucket number, dest the UUID of the intended " - + "destination, ttl controls the number of hops the message may take, data serialized data " - + "associated with this bucket, encoded using base64" - ) - @Consumes(MediaType.APPLICATION_OCTET_STREAM) - public void sessionData(@QueryParam("bucket") Integer bucket, - @QueryParam("dest") UUID dest, - @QueryParam("ttl") int ttl, - byte[] data) { - Bucket.sessionData(bucket, dest, ttl, data); - } - - /** - * Handle the '/session/insertDrools' REST call. - */ - @POST - @Path("/session/insertDrools") - @ApiOperation( - value = "Perform an incoming /session/insertDrools request", - notes = "An incoming /session/insertDrools message was received, the request data include " - + "keyword the keyword associated with the incoming object, sessionName encoded session name " - + "(groupId:artifactId:droolsSession), bucket the bucket associated with keyword, " - + "ttl controls the number of hops the message may take, data base64-encoded serialized data " - + "for the object" - ) - @Consumes(MediaType.APPLICATION_OCTET_STREAM) - public void insertDrools(@QueryParam("keyword") String keyword, - @QueryParam("session") String sessionName, - @QueryParam("bucket") int bucket, - @QueryParam("ttl") int ttl, - byte[] data) { - FeatureServerPool.incomingInsertDrools(keyword, sessionName, bucket, ttl, data); - } - - /** - * Handle the '/lock/lock' REST call. - */ - @GET - @Path("/lock/lock") - @ApiOperation( - value = "Perform an incoming /lock/lock request", - notes = "An incoming /lock/lock REST message is received, the request data include " - + "key string identifying the lock, which must hash to a bucket owned by the current host, " - + "ownerKey string key identifying the owner, uuid the UUID that uniquely identifies " - + "the original 'TargetLock', waitForLock this controls the behavior when 'key' is already " - + "locked - 'true' means wait for it to be freed, 'false' means fail, ttl controls the number " - + "of hops the message may take, the response is the message should be passed back to the " - + "requestor" - ) - @Consumes(MediaType.APPLICATION_OCTET_STREAM) - @Produces(MediaType.APPLICATION_OCTET_STREAM) - public Response lock(@QueryParam("key") String key, - @QueryParam("owner") String keyOwner, - @QueryParam("uuid") UUID uuid, - @QueryParam("wait") boolean waitForLock, - @QueryParam("ttl") int ttl) { - return TargetLock.incomingLock(key, keyOwner, uuid, waitForLock, ttl); - } - - /** - * Handle the '/lock/free' REST call. - */ - @GET - @Path("/lock/free") - @ApiOperation( - value = "Perform an incoming /lock/free request", - notes = "An incoming /lock/free REST message is received, the request data include " - + "key string identifying the lock, which must hash to a bucket owned by the current host, " - + "ownerKey string key identifying the owner, uuid the UUID that uniquely identifies " - + "the original 'TargetLock', ttl controls the number of hops the message may take, " - + "the response is the message should be passed back to the requestor" - ) - @Consumes(MediaType.APPLICATION_OCTET_STREAM) - @Produces(MediaType.APPLICATION_OCTET_STREAM) - public Response free(@QueryParam("key") String key, - @QueryParam("owner") String keyOwner, - @QueryParam("uuid") UUID uuid, - @QueryParam("ttl") int ttl) { - return TargetLock.incomingFree(key, keyOwner, uuid, ttl); - } - - /** - * Handle the '/lock/locked' REST call. - */ - @GET - @Path("/lock/locked") - @ApiOperation( - value = "Perform an incoming /lock/locked request, (this is a callback to an earlier " - + "requestor that the lock is now available)", - notes = "An incoming /lock/locked REST message is received, the request data include " - + "key string key identifying the lock, ownerKey string key identifying the owner " - + "which must hash to a bucket owned by the current host (it is typically a 'RequestID') " - + "uuid the UUID that uniquely identifies the original 'TargetLock', ttl controls the " - + "number of hops the message may take, the response is the message should be passed back " - + "to the requestor" - ) - @Consumes(MediaType.APPLICATION_OCTET_STREAM) - @Produces(MediaType.APPLICATION_OCTET_STREAM) - public Response locked(@QueryParam("key") String key, - @QueryParam("owner") String keyOwner, - @QueryParam("uuid") UUID uuid, - @QueryParam("ttl") int ttl) { - return TargetLock.incomingLocked(key, keyOwner, uuid, ttl); - } - - /** - * Handle the '/lock/audit' REST call. - */ - @POST - @Path("/lock/audit") - @ApiOperation( - value = "Perform an incoming /lock/audit request", - notes = "An incoming /lock/audit REST message is received, the request data include " - + "serverUuid the UUID of the intended destination server, ttl controls the number of hops, " - + "encodedData base64-encoded data, containing a serialized 'AuditData' instance " - + "the response is a serialized and base64-encoded 'AuditData'" - ) - @Consumes(MediaType.APPLICATION_OCTET_STREAM) - @Produces(MediaType.APPLICATION_OCTET_STREAM) - public byte[] lockAudit(@QueryParam("server") UUID server, - @QueryParam("ttl") int ttl, - byte[] data) { - return TargetLock.Audit.incomingAudit(server, ttl, data); - } - - /* ============================================================ */ - - /** - * Handle the '/cmd/dumpHosts' REST call. - */ - @GET - @Path("/cmd/dumpHosts") - @ApiOperation( - value = "Perform an incoming /cmd/dumpHosts request", - notes = "Dump out the current 'servers' table in a human-readable table form" - ) - @Produces(MediaType.TEXT_PLAIN) - public String dumpHosts() { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - Server.dumpHosts(new PrintStream(bos, true)); - return bos.toString(StandardCharsets.UTF_8); - } - - /** - * Handle the '/cmd/dumpBuckets' REST call. - */ - @GET - @Path("/cmd/dumpBuckets") - @ApiOperation( - value = "Perform an incoming /cmd/dumpBuckets request", - notes = "Dump out buckets information in a human-readable form" - ) - @Produces(MediaType.TEXT_PLAIN) - public String dumpBuckets() { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - Bucket.dumpBuckets(new PrintStream(bos, true)); - return bos.toString(StandardCharsets.UTF_8); - } - - /** - * Handle the '/cmd/ping' REST call. - */ - @GET - @Path("/cmd/ping") - @ApiOperation( - value = "Perform an incoming /cmd/ping request", - notes = "Send information about 'thisServer' to the list of hosts" - ) - @Produces(MediaType.TEXT_PLAIN) - public String ping(@QueryParam("hosts") String hosts) { - logger.info("Running '/cmd/ping', hosts={}", hosts); - - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - Server.pingHosts(new PrintStream(bos, true), hosts); - return bos.toString(StandardCharsets.UTF_8); - } - - /** - * Handle the '/cmd/bucketMessage' REST call. - */ - @GET - @Path("/cmd/bucketMessage") - @ApiOperation( - value = "Perform an incoming /cmd/bucketMessage request", - notes = "This is only used for testing the routing of messages between servers" - ) - @Produces(MediaType.TEXT_PLAIN) - public String bucketMessage(@QueryParam("keyword") String keyword, - @QueryParam("message") String message) { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - Bucket.bucketMessage(new PrintStream(bos, true), keyword, message); - return bos.toString(StandardCharsets.UTF_8); - } - - /** - * Handle the '/bucket/bucketResponse' REST call. - */ - @POST - @Path("/bucket/bucketResponse") - @ApiOperation( - value = "Perform an incoming /cmd/bucketResponse request", - notes = "This runs on the destination host, and is the continuation of an operation " - + "triggered by the /cmd/bucketMessage REST message running on the originating host" - ) - @Consumes(MediaType.TEXT_PLAIN) - @Produces(MediaType.TEXT_PLAIN) - public String bucketResponse(@QueryParam("bucket") Integer bucket, - @QueryParam("keyword") String keyword, - byte[] data) { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - Bucket.bucketResponse(new PrintStream(bos, true), bucket, keyword, data); - return bos.toString(StandardCharsets.UTF_8); - } - - /** - * Handle the '/lock/moveBucket' REST call. - */ - @GET - @Path("/cmd/moveBucket") - @ApiOperation( - value = "Perform an incoming /cmd/moveBucket request", - notes = "This is only used for testing bucket migration. It only works on the lead server" - ) - @Produces(MediaType.TEXT_PLAIN) - public String moveBucket(@QueryParam("bucket") Integer bucketNumber, - @QueryParam("host") String newHost) { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - Bucket.moveBucket(new PrintStream(bos, true), bucketNumber, newHost); - return bos.toString(StandardCharsets.UTF_8); - } - - /** - * Handle the '/lock/dumpBucketAdjuncts' REST call. - */ - @GET - @Path("/cmd/dumpBucketAdjuncts") - @ApiOperation( - value = "Perform an incoming /cmd/dumpBucketAdjuncts request", - notes = "Dump out all buckets with adjuncts" - ) - @Produces(MediaType.TEXT_PLAIN) - public String dumpBucketAdjuncts() { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - Bucket.dumpAdjuncts(new PrintStream(bos, true)); - return bos.toString(StandardCharsets.UTF_8); - } - - /** - * Handle the '/lock/dumpLocks' REST call. - */ - @GET - @Path("/cmd/dumpLocks") - @ApiOperation( - value = "Perform an incoming /cmd/dumpLocks request", - notes = "Dump out locks info, detail 'true' provides additional bucket and host information" - ) - @Produces(MediaType.TEXT_PLAIN) - public String dumpLocks(@QueryParam("detail") boolean detail) - throws IOException, InterruptedException, ClassNotFoundException { - - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - TargetLock.DumpLocks.dumpLocks(new PrintStream(bos, true), detail); - return bos.toString(StandardCharsets.UTF_8); - } - - /** - * Handle the '/lock/dumpLocksData' REST call. - */ - @GET - @Path("/lock/dumpLocksData") - @ApiOperation( - value = "Perform an incoming /cmd/dumpLocksData request", - notes = "Generate a byte stream containing serialized 'HostData'" - ) - @Produces(MediaType.APPLICATION_OCTET_STREAM) - public String dumpLocksData(@QueryParam("server") UUID server, - @QueryParam("ttl") int ttl) throws IOException { - return new String(TargetLock.DumpLocks.dumpLocksData(server, ttl), StandardCharsets.UTF_8); - } -} diff --git a/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/Server.java b/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/Server.java deleted file mode 100644 index 61a950ae..00000000 --- a/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/Server.java +++ /dev/null @@ -1,1361 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * feature-server-pool - * ================================================================================ - * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved. - * ================================================================================ - * 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. - * ============LICENSE_END========================================================= - */ - -package org.onap.policy.drools.serverpool; - -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DEFAULT_HTTPS; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DEFAULT_SELF_SIGNED_CERTIFICATES; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DEFAULT_SERVER_ADAPTIVE_GAP_ADJUST; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DEFAULT_SERVER_CONNECT_TIMEOUT; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DEFAULT_SERVER_INITIAL_ALLOWED_GAP; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DEFAULT_SERVER_IP_ADDRESS; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DEFAULT_SERVER_PORT; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DEFAULT_SERVER_READ_TIMEOUT; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DEFAULT_SERVER_THREADS_CORE_POOL_SIZE; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DEFAULT_SERVER_THREADS_KEEP_ALIVE_TIME; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DEFAULT_SERVER_THREADS_MAXIMUM_POOL_SIZE; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.HOST_LIST; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.SERVER_ADAPTIVE_GAP_ADJUST; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.SERVER_CONNECT_TIMEOUT; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.SERVER_HTTPS; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.SERVER_INITIAL_ALLOWED_GAP; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.SERVER_IP_ADDRESS; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.SERVER_PORT; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.SERVER_READ_TIMEOUT; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.SERVER_SELF_SIGNED_CERTIFICATES; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.SERVER_THREADS_CORE_POOL_SIZE; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.SERVER_THREADS_KEEP_ALIVE_TIME; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.SERVER_THREADS_MAXIMUM_POOL_SIZE; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.SITE_IP_ADDRESS; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.SITE_PORT; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.getProperty; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.io.PrintStream; -import java.io.StringReader; -import java.lang.reflect.Field; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.UnknownHostException; -import java.nio.charset.StandardCharsets; -import java.text.SimpleDateFormat; -import java.util.Base64; -import java.util.Collection; -import java.util.Date; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.Objects; -import java.util.Properties; -import java.util.TreeMap; -import java.util.TreeSet; -import java.util.UUID; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.FutureTask; -import java.util.concurrent.LinkedTransferQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import javax.ws.rs.client.Client; -import javax.ws.rs.client.Entity; -import javax.ws.rs.client.WebTarget; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import lombok.Setter; -import org.glassfish.jersey.client.ClientProperties; -import org.onap.policy.common.endpoints.event.comm.bus.internal.BusTopicParams; -import org.onap.policy.common.endpoints.http.client.HttpClient; -import org.onap.policy.common.endpoints.http.client.HttpClientConfigException; -import org.onap.policy.common.endpoints.http.client.HttpClientFactoryInstance; -import org.onap.policy.common.endpoints.http.server.HttpServletServer; -import org.onap.policy.common.endpoints.http.server.HttpServletServerFactoryInstance; -import org.onap.policy.drools.utils.PropertyUtil; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class Server implements Comparable<Server> { - private static Logger logger = LoggerFactory.getLogger(Server.class); - - // maps UUID to Server object for all known servers - private static TreeMap<UUID, Server> servers = - new TreeMap<>(Util.uuidComparator); - - // maps UUID to Server object for all failed servers - // (so they aren't accidentally restored, due to updates from other hosts) - private static TreeMap<UUID, Server> failedServers = - new TreeMap<>(Util.uuidComparator); - - // subset of servers to be notified (null means it needs to be rebuilt) - @Setter - private static LinkedList<Server> notifyList = null; - - // data to be sent out to notify list - private static TreeSet<Server> updatedList = new TreeSet<>(); - - // the server associated with the current host - private static Server thisServer = null; - - // the current REST server - private static HttpServletServer restServer; - - /*==================================================*/ - /* Some properties extracted at initialization time */ - /*==================================================*/ - - // initial value of gap to allow between pings - private static long initialAllowedGap; - - // used in adaptive calculation of allowed gap between pings - private static long adaptiveGapAdjust; - - // time to allow for TCP connect (long) - private static String connectTimeout; - - // time to allow before TCP read timeout (long) - private static String readTimeout; - - // outgoing per-server thread pool parameters - private static int corePoolSize; - private static int maximumPoolSize; - private static long keepAliveTime; - - // https-related parameters - private static boolean useHttps; - private static boolean useSelfSignedCertificates; - - // list of remote host names - private static String[] hostList = new String[0]; - - /*=========================================================*/ - /* Fields included in every 'ping' message between servers */ - /*=========================================================*/ - - // unique id for this server - private UUID uuid; - - // counter periodically incremented to indicate the server is "alive" - private int count; - - // 16 byte MD5 checksum over additional data that is NOT included in - // every 'ping' message -- used to determine whether the data is up-to-date - private byte[] checksum; - - /*========================================================================*/ - /* The following data is included in the checksum, and doesn't change too */ - /* frequently (some fields may change as servers go up and down) */ - /*========================================================================*/ - - // IP address and port of listener - private InetSocketAddress socketAddress; - - // site IP address and port - private InetSocketAddress siteSocketAddress = null; - - /*============================================*/ - /* Local information not included in checksum */ - /*============================================*/ - - // destination socket information - private InetSocketAddress destSocketAddress; - private String destName; - - // REST client fields - private HttpClient client; - private WebTarget target; - private ThreadPoolExecutor sendThreadPool = null; - - // time when the 'count' field was last updated - private long lastUpdateTime; - - // calculated field indicating the maximum time between updates - private long allowedGap = initialAllowedGap; - - // indicates whether the 'Server' instance is active or not (synchronized) - private boolean active = true; - - /* - * Tags for encoding of server data - */ - static final int END_OF_PARAMETERS_TAG = 0; - static final int SOCKET_ADDRESS_TAG = 1; - static final int SITE_SOCKET_ADDRESS_TAG = 2; - - // 'pingHosts' error - static final String PINGHOSTS_ERROR = "Server.pingHosts error"; - - // a string for print - static final String PRINTOUT_DASHES = "-------"; - - /*==============================*/ - /* Comparable<Server> interface */ - /*==============================*/ - - /** - * Compare this instance to another one by comparing the 'uuid' field. - */ - @Override - public int compareTo(Server other) { - return Util.uuidComparator.compare(uuid, other.uuid); - } - - @Override - public int hashCode() { - return Objects.hash(uuid); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (!(obj instanceof Server)) { - return false; - } - Server other = (Server) obj; - return Objects.equals(uuid, other.uuid); - } - - /** - * This method may be invoked from any thread, and is used as the main - * entry point when testing. - * - * @param args arguments containing an '=' character are interpreted as - * a property, other arguments are presumed to be a property file. - */ - public static void main(String[] args) throws IOException { - Properties prop = new Properties(); - - for (String arg : args) { - // arguments with an equals sign in them are a property definition - - // otherwise, they are a properties file name - - if (arg.contains("=")) { - prop.load(new StringReader(arg)); - } else { - prop.putAll(PropertyUtil.getProperties(arg)); - } - } - - String rval = startup(prop); - if (rval != null) { - logger.error("Server.startup failed: {}", rval); - } - } - - /** - * This method may be invoked from any thread, and performs initialization. - * - * @param propertiesFile the name of a property file - */ - public static String startup(String propertiesFile) { - Properties properties; - try { - properties = PropertyUtil.getProperties(propertiesFile); - } catch (IOException e) { - logger.error("Server.startup: exception reading properties", e); - properties = new Properties(); - } - return startup(properties); - } - - /** - * This method may be invoked from any thread, and performs initialization. - * - * @param properties contains properties used by the server - */ - public static String startup(Properties properties) { - ServerPoolProperties.setProperties(properties); - logger.info("startup: properties={}", properties); - - // fetch some static properties - initialAllowedGap = getProperty(SERVER_INITIAL_ALLOWED_GAP, - DEFAULT_SERVER_INITIAL_ALLOWED_GAP); - adaptiveGapAdjust = getProperty(SERVER_ADAPTIVE_GAP_ADJUST, - DEFAULT_SERVER_ADAPTIVE_GAP_ADJUST); - connectTimeout = - String.valueOf(getProperty(SERVER_CONNECT_TIMEOUT, - DEFAULT_SERVER_CONNECT_TIMEOUT)); - readTimeout = String.valueOf(getProperty(SERVER_READ_TIMEOUT, - DEFAULT_SERVER_READ_TIMEOUT)); - corePoolSize = getProperty(SERVER_THREADS_CORE_POOL_SIZE, - DEFAULT_SERVER_THREADS_CORE_POOL_SIZE); - maximumPoolSize = getProperty(SERVER_THREADS_MAXIMUM_POOL_SIZE, - DEFAULT_SERVER_THREADS_MAXIMUM_POOL_SIZE); - keepAliveTime = getProperty(SERVER_THREADS_KEEP_ALIVE_TIME, - DEFAULT_SERVER_THREADS_KEEP_ALIVE_TIME); - useHttps = getProperty(SERVER_HTTPS, DEFAULT_HTTPS); - useSelfSignedCertificates = getProperty(SERVER_SELF_SIGNED_CERTIFICATES, - DEFAULT_SELF_SIGNED_CERTIFICATES); - String hostListNames = getProperty(HOST_LIST, null); - if (hostListNames != null) { - hostList = hostListNames.split(","); - } - - String possibleError = null; - try { - // fetch server information - String ipAddressString = - getProperty(SERVER_IP_ADDRESS, DEFAULT_SERVER_IP_ADDRESS); - int port = getProperty(SERVER_PORT, DEFAULT_SERVER_PORT); - - possibleError = "Unknown Host: " + ipAddressString; - InetAddress address = InetAddress.getByName(ipAddressString); - InetSocketAddress socketAddress = new InetSocketAddress(address, port); - - restServer = HttpServletServerFactoryInstance.getServerFactory().build( - "SERVER-POOL", // name - useHttps, // https - socketAddress.getAddress().getHostAddress(), // host (maybe 0.0.0.0) - port, // port (can no longer be 0) - null, // contextPath - false, // swagger - false); // managed - restServer.addServletClass(null, RestServerPool.class.getName()); - - // add any additional servlets - for (ServerPoolApi feature : ServerPoolApi.impl.getList()) { - for (Class<?> clazz : feature.servletClasses()) { - restServer.addServletClass(null, clazz.getName()); - } - } - - // we may not know the port until after the server is started - restServer.start(); - possibleError = null; - - // determine the address to use - if (DEFAULT_SERVER_IP_ADDRESS.contentEquals(address.getHostAddress())) { - address = InetAddress.getLocalHost(); - } - - thisServer = new Server(new InetSocketAddress(address, port)); - - // TBD: is this really appropriate? - thisServer.newServer(); - - // start background thread - MainLoop.startThread(); - MainLoop.queueWork(() -> { - // run this in the 'MainLoop' thread - Leader.startup(); - Bucket.startup(); - }); - logger.info("Listening on port {}", port); - - return null; - } catch (UnknownHostException e) { - logger.error("Server.startup: exception start server", e); - if (possibleError == null) { - possibleError = e.toString(); - } - return possibleError; - } - } - - /** - * Shut down all threads associate with server pool. - */ - public static void shutdown() { - Discovery.stopDiscovery(); - MainLoop.stopThread(); - TargetLock.shutdown(); - Util.shutdown(); - - HashSet<Server> allServers = new HashSet<>(); - allServers.addAll(servers.values()); - allServers.addAll(failedServers.values()); - - for (Server server : allServers) { - if (server.sendThreadPool != null) { - server.sendThreadPool.shutdown(); - } - } - if (restServer != null) { - restServer.shutdown(); - } - } - - /** - * Return the Server instance associated with the current host. - * - * @return the Server instance associated with the current host - */ - public static Server getThisServer() { - return thisServer; - } - - /** - * Return the first Server instance in the 'servers' list. - * - * @return the first Server instance in the 'servers' list - * (the one with the lowest UUID) - */ - public static Server getFirstServer() { - return servers.firstEntry().getValue(); - } - - /** - * Lookup a Server instance associated with a UUID. - * - * @param uuid the key to the lookup - @ @return the associated 'Server' instance, or 'null' if none - */ - public static Server getServer(UUID uuid) { - return servers.get(uuid); - } - - /** - * Return a count of the number of servers. - * - * @return a count of the number of servers - */ - public static int getServerCount() { - return servers.size(); - } - - /** - * Return the complete list of servers. - * - * @return the complete list of servers - */ - public static Collection<Server> getServers() { - return servers.values(); - } - - /** - * This method is invoked from the 'startup' thread, and creates a new - * 'Server' instance for the current server. - * - * @param socketAddress the IP address and port the listener is bound to - */ - private Server(InetSocketAddress socketAddress) { - this.uuid = UUID.randomUUID(); - this.count = 1; - this.socketAddress = socketAddress; - this.lastUpdateTime = System.currentTimeMillis(); - - // site information - - String siteIp = getProperty(SITE_IP_ADDRESS, null); - int sitePort = getProperty(SITE_PORT, 0); - if (siteIp != null && sitePort != 0) { - // we do have site information specified - try { - siteSocketAddress = new InetSocketAddress(siteIp, sitePort); - if (siteSocketAddress.getAddress() == null) { - logger.error("Couldn't resolve site address: {}", siteIp); - siteSocketAddress = null; - } - } catch (IllegalArgumentException e) { - logger.error("Illegal 'siteSocketAddress'", e); - siteSocketAddress = null; - } - } - - // TBD: calculate checksum - } - - /** - * Initialize a 'Server' instance from a 'DataInputStream'. If it is new, - * it may get inserted in the table. If it is an update, fields in an - * existing 'Server' may be updated. - * - * @param is the 'DataInputStream' - */ - Server(DataInputStream is) throws IOException { - // read in 16 byte UUID - uuid = Util.readUuid(is); - - // read in 4 byte counter value - count = is.readInt(); - - // read in 16 byte MD5 checksum - checksum = new byte[16]; - is.readFully(checksum); - - // optional parameters - int tag; - while ((tag = is.readUnsignedByte()) != END_OF_PARAMETERS_TAG) { - switch (tag) { - case SOCKET_ADDRESS_TAG: - socketAddress = readSocketAddress(is); - break; - case SITE_SOCKET_ADDRESS_TAG: - siteSocketAddress = readSocketAddress(is); - break; - default: - // ignore tag - logger.error("Illegal tag: {}", tag); - break; - } - } - } - - /** - * Read an 'InetSocketAddress' from a 'DataInputStream'. - * - * @param is the 'DataInputStream' - * @return the 'InetSocketAddress' - */ - private static InetSocketAddress readSocketAddress(DataInputStream is) throws IOException { - - byte[] ipAddress = new byte[4]; - is.read(ipAddress, 0, 4); - int port = is.readUnsignedShort(); - return new InetSocketAddress(InetAddress.getByAddress(ipAddress), port); - } - - /** - * {@inheritDoc} - */ - @Override - public String toString() { - return "Server[" + uuid + "]"; - } - - /** - * Return the UUID associated with this Server. - * - * @return the UUID associated with this Server - */ - public UUID getUuid() { - return uuid; - } - - /** - * Return the external InetSocketAddress of the site. - * - * @return the external InetSocketAddress of the site - * ('null' if it doesn't exist) - */ - public InetSocketAddress getSiteSocketAddress() { - return siteSocketAddress; - } - - /** - * This method may be called from any thread. - * - * @return 'true' if the this server is active, and 'false' if not - */ - public synchronized boolean isActive() { - return active; - } - - /** - * This method writes out the data associated with the current Server - * instance. - * - * @param os outout stream that should receive the data - */ - void writeServerData(DataOutputStream os) throws IOException { - // write out 16 byte UUID - Util.writeUuid(os, uuid); - - // write out 4 byte counter value - os.writeInt(count); - - // write out 16 byte MD5 checksum - // TBD: should this be implemented? - os.write(checksum == null ? new byte[16] : checksum); - - if (socketAddress != null) { - // write out socket address - os.writeByte(SOCKET_ADDRESS_TAG); - os.write(socketAddress.getAddress().getAddress(), 0, 4); - os.writeShort(socketAddress.getPort()); - } - - if (siteSocketAddress != null) { - // write out socket address - os.writeByte(SITE_SOCKET_ADDRESS_TAG); - os.write(siteSocketAddress.getAddress().getAddress(), 0, 4); - os.writeShort(siteSocketAddress.getPort()); - } - - os.writeByte(END_OF_PARAMETERS_TAG); - } - - /** - * Do any processing needed to create a new server. This method is invoked - * from the 'MainLoop' thread in every case except for the current server, - * in which case it is invoked in 'startup' prior to creating 'MainLoop'. - */ - private void newServer() { - Server failed = failedServers.get(uuid); - if (failed != null) { - // this one is on the failed list -- see if the counter has advanced - if ((count - failed.count) <= 0) { - // the counter has not advanced -- ignore - return; - } - - // the counter has advanced -- somehow, this server has returned - failedServers.remove(uuid); - synchronized (this) { - active = true; - } - logger.error("Server reawakened: {} ({})", uuid, socketAddress); - } - - lastUpdateTime = System.currentTimeMillis(); - servers.put(uuid, this); - updatedList.add(this); - - // notify list will need to be rebuilt - setNotifyList(null); - - if (socketAddress != null && this != thisServer) { - // initialize 'client' and 'target' fields - if (siteSocketAddress != null - && !siteSocketAddress.equals(thisServer.siteSocketAddress)) { - // destination is on a remote site - destSocketAddress = siteSocketAddress; - } else { - // destination is on the local site -- use direct addressing - destSocketAddress = socketAddress; - } - destName = socketAddressToName(destSocketAddress); - try { - // 'client' is used for REST messages to the destination - client = buildClient(uuid.toString(), destSocketAddress, destName); - - // initialize the 'target' field - target = getTarget(client); - } catch (NoSuchFieldException | IllegalAccessException | HttpClientConfigException e) { - logger.error("Server.newServer: problems creating 'client'", e); - } - } - logger.info("New server: {} ({})", uuid, socketAddress); - for (Events listener : Events.getListeners()) { - listener.newServer(this); - } - } - - /** - * Check the server state in response to some issue. At present, only the - * 'destName' information is checked. - */ - private void checkServer() { - // recalculate 'destName' (we have seen DNS issues) - String newDestName = socketAddressToName(destSocketAddress); - if (newDestName.equals(destName)) { - return; - } - logger.warn("Remote host name for {} has changed from {} to {}", - destSocketAddress, destName, newDestName); - - // shut down old client, and rebuild - client.shutdown(); - client = null; - target = null; - - // update 'destName', and rebuild the client - destName = newDestName; - try { - // 'client' is used for REST messages to the destination - client = buildClient(uuid.toString(), destSocketAddress, destName); - - // initialize the 'target' field - target = getTarget(client); - } catch (NoSuchFieldException | IllegalAccessException | HttpClientConfigException e) { - logger.error("Server.checkServer: problems recreating 'client'", e); - } - } - - /** - * Update server data. - * - * @param serverData this is a temporary 'Server' instance created from - * an incoming message, which is used to update fields within the - * 'Server' instance identified by 'this' - */ - private void updateServer(Server serverData) { - if (serverData.count > count) { - // an update has occurred - count = serverData.count; - - // TBD: calculate and verify checksum, more fields may be updated - - // adjust 'allowedGap' accordingly - long currentTime = System.currentTimeMillis(); - long gap = currentTime - lastUpdateTime; - - // adjust 'allowedGap' accordingly - // TBD: need properties to support overrides - gap = gap * 3 / 2 + adaptiveGapAdjust; - if (gap > allowedGap) { - // update 'allowedGap' immediately - allowedGap = gap; - } else { - // gradually pull the allowed gap down - // TBD: need properties to support overrides - allowedGap = (allowedGap * 15 + gap) / 16; - } - lastUpdateTime = currentTime; - - updatedList.add(this); - } - } - - /** - * a server has failed. - */ - private void serverFailed() { - // mark as inactive - synchronized (this) { - active = false; - } - - // remove it from the table - servers.remove(uuid); - - // add it to the failed servers table - failedServers.put(uuid, this); - - // clean up client information - if (client != null) { - client.shutdown(); - client = null; - target = null; - } - - // log an error message - logger.error("Server failure: {} ({})", uuid, socketAddress); - for (Events listener : Events.getListeners()) { - listener.serverFailed(this); - } - } - - /** - * Fetch, and possibly calculate, the "notify list" associated with this - * server. This is the list of servers to forward a server and bucket - * information to, and is approximately log2(n) in length, where 'n' is - * the total number of servers. - * It is calculated by starting with all of the servers sorted by UUID -- - * let's say the current server is at position 's'. The notify list will - * contain the server at positions: - * (s + 1) % n - * (s + 2) % n - * (s + 4) % n - * ... - * Using all powers of 2 less than 'n'. If the total server count is 50, - * this list has 6 entries. - * @return the notify list - */ - static Collection<Server> getNotifyList() { - // The 'notifyList' value is initially 'null', and it is reset to 'null' - // every time a new host joins, or one leaves. That way, it is marked for - // recalculation, but only when needed. - if (notifyList != null) { - return notifyList; - } - - // next index we are looking for - int dest = 1; - - // our current position in the Server table -- starting at 'thisServer' - UUID current = thisServer.uuid; - - // site socket address of 'current' - InetSocketAddress thisSiteSocketAddress = thisServer.siteSocketAddress; - - // hash set of all site socket addresses located - HashSet<InetSocketAddress> siteSocketAddresses = new HashSet<>(); - siteSocketAddresses.add(thisSiteSocketAddress); - - // the list we are building - notifyList = new LinkedList<>(); - - int index = 1; - for ( ; ; ) { - // move to the next key (UUID) -- if we hit the end of the table, - // wrap to the beginning - current = servers.higherKey(current); - if (current == null) { - current = servers.firstKey(); - } - if (current.equals(thisServer.uuid)) { - // we have looped through the entire list - break; - } - - // fetch associated server & site socket address - Server server = servers.get(current); - InetSocketAddress currentSiteSocketAddress = - server.siteSocketAddress; - - if (Objects.equals(thisSiteSocketAddress, - currentSiteSocketAddress)) { - // same site -- see if we should add this one - if (index == dest) { - // this is the next index we are looking for -- - // add the server - notifyList.add(server); - - // advance to the next offset (current-offset * 2) - dest = dest << 1; - } - index += 1; - } else if (!siteSocketAddresses.contains(currentSiteSocketAddress)) { - // we need at least one member from each site - notifyList.add(server); - siteSocketAddresses.add(currentSiteSocketAddress); - } - } - - return notifyList; - } - - /** - * See if there is a host name associated with a destination socket address. - * - * @param dest the socket address of the destination - * @return the host name associated with the IP address, or the IP address - * if no associated host name is found. - */ - private static String socketAddressToName(InetSocketAddress dest) { - // destination IP address - InetAddress inetAddress = dest.getAddress(); - String destName = null; - - // go through the 'hostList' to see if there is a matching name - for (String hostName : hostList) { - try { - if (inetAddress.equals(InetAddress.getByName(hostName))) { - // this one matches -- use the name instead of the IP address - destName = hostName; - break; - } - } catch (UnknownHostException e) { - logger.debug("Server.socketAddressToName error", e); - } - } - - // default name = string value of IP address - return destName == null ? inetAddress.getHostAddress() : destName; - } - - /** - * Create an 'HttpClient' instance for a particular host. - * - * @param name of the host (currently a UUID or host:port string) - * @param dest the socket address of the destination - * @param destName the string name to use for the destination - */ - static HttpClient buildClient(String name, InetSocketAddress dest, String destName) - throws HttpClientConfigException { - - return HttpClientFactoryInstance.getClientFactory().build( - BusTopicParams.builder() - .clientName(name) // name - .useHttps(useHttps) // https - .allowSelfSignedCerts(useSelfSignedCertificates)// selfSignedCerts - .hostname(destName) // host - .port(dest.getPort()) // port - .managed(false) // managed - .build()); - } - - /** - * Extract the 'WebTarget' information from the 'HttpClient'. - * - * @param client the associated HttpClient instance - * @return a WebTarget referring to the previously-specified 'baseUrl' - */ - static WebTarget getTarget(HttpClient client) - throws NoSuchFieldException, IllegalAccessException { - // need access to the internal field 'client' - // TBD: We need a way to get this information without reflection - Field field = client.getClass().getDeclaredField("client"); - field.setAccessible(true); - Client rsClient = (Client) field.get(client); - field.setAccessible(false); - - rsClient.property(ClientProperties.CONNECT_TIMEOUT, connectTimeout); - rsClient.property(ClientProperties.READ_TIMEOUT, readTimeout); - - // For performance reasons, the root 'WebTarget' is generated only once - // at initialization time for each remote host. - return rsClient.target(client.getBaseUrl()); - } - - /** - * This method may be invoked from any thread, and is used to send a - * message to the destination server associated with this 'Server' instance. - * - * @param path the path relative to the base URL - * @param entity the "request entity" containing the body of the - * HTTP POST request - */ - public void post(final String path, final Entity<?> entity) { - post(path, entity, null); - } - - /** - * This method may be invoked from any thread, and is used to send a - * message to the destination server associated with this 'Server' instance. - * - * @param path the path relative to the base URL - * @param entity the "request entity" containing the body of the - * HTTP POST request (if 'null', an HTTP GET is used instead) - * @param responseCallback if non-null, this callback may be used to - * modify the WebTarget, and/or receive the POST response message - */ - public void post(final String path, final Entity<?> entity, - PostResponse responseCallback) { - if (target == null) { - return; - } - - getThreadPool().execute(() -> { - /* - * This method is running within the 'MainLoop' thread. - */ - try { - invokeWebTarget(path, entity, responseCallback); - - } catch (Exception e) { - logger.error("Failed to send to {} ({}, {})", - uuid, destSocketAddress, destName); - if (responseCallback != null) { - responseCallback.exceptionResponse(e); - } - // this runs in the 'MainLoop' thread - - // the DNS cache may have been out-of-date when this server - // was first contacted -- fix the problem, if needed - MainLoop.queueWork(this::checkServer); - } - }); - } - - private void invokeWebTarget(final String path, final Entity<?> entity, PostResponse responseCallback) { - WebTarget webTarget = target.path(path); - if (responseCallback != null) { - // give callback a chance to modify 'WebTarget' - webTarget = responseCallback.webTarget(webTarget); - - // send the response to the callback - Response response; - if (entity == null) { - response = webTarget.request().get(); - } else { - response = webTarget.request().post(entity); - } - responseCallback.response(response); - } else { - // just do the invoke, and ignore the response - if (entity == null) { - webTarget.request().get(); - } else { - webTarget.request().post(entity); - } - } - } - - /** - * This method may be invoked from any thread. - * - * @return the 'ThreadPoolExecutor' associated with this server - */ - public synchronized ThreadPoolExecutor getThreadPool() { - if (sendThreadPool == null) { - // build a thread pool for this Server - sendThreadPool = - new ThreadPoolExecutor(corePoolSize, maximumPoolSize, - keepAliveTime, TimeUnit.MILLISECONDS, - new LinkedTransferQueue<>()); - sendThreadPool.allowCoreThreadTimeOut(true); - } - return sendThreadPool; - } - - /** - * Lower-level method supporting HTTP, which requires that the caller's - * thread tolerate blocking. This method may be called from any thread. - * - * @param path the path relative to the base URL - * @return a 'WebTarget' instance pointing to this path - */ - public WebTarget getWebTarget(String path) { - return target == null ? null : target.path(path); - } - - /** - * This method may be invoked from any thread, but its real intent is - * to decode an incoming 'admin' message (which is Base-64-encoded), - * and send it to the 'MainLoop' thread for processing. - * - * @param data the base-64-encoded data - */ - static void adminRequest(byte[] data) { - final byte[] packet = Base64.getDecoder().decode(data); - Runnable task = () -> { - try { - ByteArrayInputStream bis = new ByteArrayInputStream(packet); - DataInputStream dis = new DataInputStream(bis); - - while (dis.available() != 0) { - Server serverData = new Server(dis); - - // TBD: Compare with current server - - Server server = servers.get(serverData.uuid); - if (server == null) { - serverData.newServer(); - } else { - server.updateServer(serverData); - } - } - } catch (Exception e) { - logger.error("Server.adminRequest: can't decode packet", e); - } - }; - MainLoop.queueWork(task); - } - - /** - * Send out information about servers 'updatedList' to all servers - * in 'notifyList' (may need to build or rebuild 'notifyList'). - */ - static void sendOutData() throws IOException { - // include 'thisServer' in the data -- first, advance the count - thisServer.count += 1; - if (thisServer.count == 0) { - /* - * counter wrapped (0 is a special case) -- - * actually, we could probably leave this out, because it would take - * more than a century to wrap if the increment is 1 second - */ - thisServer.count = 1; - } - - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - DataOutputStream dos = new DataOutputStream(bos); - - thisServer.lastUpdateTime = System.currentTimeMillis(); - thisServer.writeServerData(dos); - - // include all hosts in the updated list - for (Server server : updatedList) { - server.writeServerData(dos); - } - updatedList.clear(); - - // create an 'Entity' that can be sent out to all hosts in the notify list - Entity<String> entity = Entity.entity( - new String(Base64.getEncoder().encode(bos.toByteArray()), StandardCharsets.UTF_8), - MediaType.APPLICATION_OCTET_STREAM_TYPE); - for (Server server : getNotifyList()) { - server.post("admin", entity); - } - } - - /** - * Search for servers which have taken too long to respond. - */ - static void searchForFailedServers() { - long currentTime = System.currentTimeMillis(); - - // used to build a list of newly-failed servers - LinkedList<Server> failed = new LinkedList<>(); - for (Server server : servers.values()) { - if (server == thisServer) { - continue; - } - long gap = currentTime - server.lastUpdateTime; - if (gap > server.allowedGap) { - // add it to the failed list -- we don't call 'serverFailed' yet, - // because this updates the server list, and leads to a - // 'ConcurrentModificationException' - failed.add(server); - } - } - - // remove servers from our list - if (!failed.isEmpty()) { - for (Server server : failed) { - server.serverFailed(); - } - notifyList = null; - } - } - - /** - * This method may be invoked from any thread: - * Send information about 'thisServer' to the list of hosts. - * - * @param out the 'PrintStream' to use for displaying information - * @param hosts a comma-separated list of entries containing - * 'host:port' or just 'port' (current host is implied in this case) - */ - static void pingHosts(PrintStream out, String hosts) { - LinkedList<InetSocketAddress> addresses = new LinkedList<>(); - boolean error = false; - - for (String host : hosts.split(",")) { - try { - String[] segs = host.split(":"); - - switch (segs.length) { - case 1: - addresses.add(new InetSocketAddress(InetAddress.getLocalHost(), - Integer.parseInt(segs[0]))); - break; - case 2: - addresses.add(new InetSocketAddress(segs[0], - Integer.parseInt(segs[1]))); - break; - default: - out.println(host + ": Invalid host/port value"); - error = true; - break; - } - } catch (NumberFormatException e) { - out.println(host + ": Invalid port value"); - logger.error(PINGHOSTS_ERROR, e); - error = true; - } catch (UnknownHostException e) { - out.println(host + ": Unknown host"); - logger.error(PINGHOSTS_ERROR, e); - error = true; - } - } - if (!error) { - pingHosts(out, addresses); - } - } - - /** - * This method may be invoked from any thread: - * Send information about 'thisServer' to the list of hosts. - * - * @param out the 'PrintStream' to use for displaying information - * @param hosts a collection of 'InetSocketAddress' instances, which are - * the hosts to send the information to - */ - static void pingHosts(final PrintStream out, - final Collection<InetSocketAddress> hosts) { - FutureTask<Integer> ft = new FutureTask<>(() -> { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - DataOutputStream dos = new DataOutputStream(bos); - - // add information for this server only - try { - thisServer.writeServerData(dos); - - // create an 'Entity' that can be sent out to all hosts - Entity<String> entity = Entity.entity( - new String(Base64.getEncoder().encode(bos.toByteArray()), - StandardCharsets.UTF_8), - MediaType.APPLICATION_OCTET_STREAM_TYPE); - pingHostsLoop(entity, out, hosts); - } catch (IOException e) { - out.println("Unable to generate 'ping' data: " + e); - logger.error(PINGHOSTS_ERROR, e); - } - return 0; - }); - - MainLoop.queueWork(ft); - try { - ft.get(60, TimeUnit.SECONDS); - } catch (InterruptedException e) { - logger.error("Server.pingHosts: interrupted waiting for queued work", e); - Thread.currentThread().interrupt(); - } catch (ExecutionException | TimeoutException e) { - logger.error("Server.pingHosts: error waiting for queued work", e); - } - } - - /** - * This method is used for pingHosts method to reduce its Cognitive Complexity. - * - * @param entity for sending out to all hosts - * @param out the 'PrintStream' to use for displaying information - * @param hosts a collection of 'InetSocketAddress' instances, which are - * the hosts to send the information to - */ - static void pingHostsLoop(final Entity<String> entity, - final PrintStream out, - final Collection<InetSocketAddress> hosts) { - // loop through hosts - for (InetSocketAddress host : hosts) { - HttpClient httpClient = null; - - try { - httpClient = buildClient(host.toString(), host, - socketAddressToName(host)); - getTarget(httpClient).path("admin").request().post(entity); - httpClient.shutdown(); - httpClient = null; - } catch (NoSuchFieldException | IllegalAccessException e) { - out.println(host + ": Unable to get link to target"); - logger.error(PINGHOSTS_ERROR, e); - } catch (Exception e) { - out.println(host + ": " + e); - logger.error(PINGHOSTS_ERROR, e); - } - if (httpClient != null) { - httpClient.shutdown(); - } - } - } - - /** - * This method may be invoked from any thread: - * Dump out the current 'servers' table in a human-readable table form. - * - * @param out the 'PrintStream' to dump the table to - */ - public static void dumpHosts(final PrintStream out) { - FutureTask<Integer> ft = new FutureTask<>(() -> { - dumpHostsInternal(out); - return 0; - }); - MainLoop.queueWork(ft); - try { - ft.get(60, TimeUnit.SECONDS); - } catch (InterruptedException e) { - logger.error("Server.dumpHosts: interrupted waiting for queued work", e); - Thread.currentThread().interrupt(); - } catch (ExecutionException | TimeoutException e) { - logger.error("Server.dumpHosts: error waiting for queued work", e); - } - } - - /** - * Dump out the current 'servers' table in a human-readable table form. - * - * @param out the 'PrintStream' to dump the table to - */ - private static void dumpHostsInternal(PrintStream out) { - // modifications to 'servers.values()' and 'notifyList'. - HashSet<Server> localNotifyList = new HashSet<>(getNotifyList()); - - // see if we have any site information - boolean siteData = false; - for (Server server : servers.values()) { - if (server.siteSocketAddress != null) { - siteData = true; - break; - } - } - - String format = "%1s %-36s %-15s %5s %5s %12s %7s %7s\n"; - SimpleDateFormat dateFormat = new SimpleDateFormat("kk:mm:ss.SSS"); - - if (siteData) { - format = "%1s %-36s %-15s %5s %-15s %5s %5s %12s %7s %7s\n"; - // @formatter:off - out.printf(format, "", "UUID", "IP Address", "Port", - "Site IP Address", "Port", - "Count", "Update Time", "Elapsed", "Allowed"); - out.printf(format, "", "----", "----------", "----", - "---------------", "----", - "-----", "-----------", PRINTOUT_DASHES, PRINTOUT_DASHES); - // @formatter:on - } else { - // @formatter:off - out.printf(format, "", "UUID", "IP Address", "Port", - "Count", "Update Time", "Elapsed", "Allowed"); - out.printf(format, "", "----", "----------", "----", - "-----", "-----------", PRINTOUT_DASHES, PRINTOUT_DASHES); - // @formatter:on - } - - long currentTime = System.currentTimeMillis(); - for (Server server : servers.values()) { - String thisOne = ""; - - if (server == thisServer) { - thisOne = "*"; - } else if (localNotifyList.contains(server)) { - thisOne = "n"; - } - - if (siteData) { - String siteIp = ""; - String sitePort = ""; - if (server.siteSocketAddress != null) { - siteIp = - server.siteSocketAddress.getAddress().getHostAddress(); - sitePort = String.valueOf(server.siteSocketAddress.getPort()); - } - - out.printf(format, thisOne, server.uuid, - server.socketAddress.getAddress().getHostAddress(), - server.socketAddress.getPort(), - siteIp, sitePort, server.count, - dateFormat.format(new Date(server.lastUpdateTime)), - currentTime - server.lastUpdateTime, - server.allowedGap); - } else { - out.printf(format, thisOne, server.uuid, - server.socketAddress.getAddress().getHostAddress(), - server.socketAddress.getPort(), server.count, - dateFormat.format(new Date(server.lastUpdateTime)), - currentTime - server.lastUpdateTime, - server.allowedGap); - } - } - out.println("Count: " + servers.size()); - } - - /* ============================================================ */ - - /** - * This interface supports the 'post' method, and provides the opportunity - * to change the WebTarget and/or receive the POST response message. - */ - interface PostResponse { - /** - * Callback that can be used to modify 'WebTarget', and do things like - * add query parameters. - * - * @param webTarget the current WebTarget - * @return the updated WebTarget - */ - public default WebTarget webTarget(WebTarget webTarget) { - return webTarget; - } - - /** - * Callback that passes the POST response. - * - * @param response the POST response - */ - public default void response(Response response) { - } - - /** - * Callback that passes the POST exception response. - * - */ - public default void exceptionResponse(Exception exception) { - Response.ResponseBuilder response; - response = Response.serverError(); - this.response(response.build()); - } - } -} diff --git a/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/ServerPoolApi.java b/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/ServerPoolApi.java deleted file mode 100644 index 8ec7c100..00000000 --- a/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/ServerPoolApi.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * feature-server-pool - * ================================================================================ - * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved. - * ================================================================================ - * 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. - * ============LICENSE_END========================================================= - */ - -package org.onap.policy.drools.serverpool; - -import java.util.Collection; -import java.util.Collections; -import org.onap.policy.common.utils.services.OrderedService; -import org.onap.policy.common.utils.services.OrderedServiceImpl; - -public abstract class ServerPoolApi implements OrderedService { - /** - * 'ServerPoolApi.impl.getList()' returns an ordered list of objects - * implementing the 'ServerPoolApi' interface. - */ - public static final OrderedServiceImpl<ServerPoolApi> impl = - new OrderedServiceImpl<>(ServerPoolApi.class); - - /** - * method gives all of the listening features the ability to add - * classes to the 'HttpServletServer'. - * - * @return a Collection of classes implementing REST methods - */ - public Collection<Class<?>> servletClasses() { - return Collections.emptyList(); - } - - /** - * This is called in the case where no bucket migration data was received - * from the old owner of the bucket (such as if the old owner failed). - * It gives one or more features the opportunity to do the restore. - * - * @param bucket the bucket that needs restoring - */ - public void restoreBucket(Bucket bucket) { - // do nothing - } - - /** - * This is called whenever a 'GlobalLocks' object is updated. It was added - * in order to support persistence, but may be used elsewhere as well. - * - * @param bucket the bucket containing the 'GlobalLocks' adjunct - * @param globalLocks the 'GlobalLocks' adjunct - */ - public void lockUpdate(Bucket bucket, TargetLock.GlobalLocks globalLocks) { - // do nothing - } - - /** - * This is called when the state of a bucket has changed, but is currently - * stable, and it gives features the ability to do an audit. The intent is - * to make sure that the adjunct state is correct; in particular, to remove - * adjuncts that should no longer be there based upon the current state. - * Note that this method is called while being synchronized on the bucket. - * - * @param bucket the bucket to audit - * @param isOwner 'true' if the current host owns the bucket - * @param isBackup 'true' if the current host is a backup for the bucket - */ - public void auditBucket(Bucket bucket, boolean isOwner, boolean isBackup) { - // do nothing - } -} diff --git a/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/ServerPoolProperties.java b/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/ServerPoolProperties.java deleted file mode 100644 index d1c09d43..00000000 --- a/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/ServerPoolProperties.java +++ /dev/null @@ -1,338 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * feature-server-pool - * ================================================================================ - * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved. - * ================================================================================ - * 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. - * ============LICENSE_END========================================================= - */ - -package org.onap.policy.drools.serverpool; - -import java.util.Properties; -import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class ServerPoolProperties { - // 'Server' port listener - public static final String SERVER_IP_ADDRESS = "server.pool.server.ipAddress"; - public static final String SERVER_PORT = "server.pool.server.port"; - public static final String SERVER_HTTPS = "server.pool.server.https"; - public static final String SERVER_SELF_SIGNED_CERTIFICATES = - "server.pool.server.selfSignedCerts"; - - // 'site' information - public static final String SITE_IP_ADDRESS = "server.pool.server.site.ip"; - public static final String SITE_PORT = "server.pool.server.site.port"; - - // the default is to listen to all IP addresses on the host - public static final String DEFAULT_SERVER_IP_ADDRESS = "0.0.0.0"; - - // the default is to dynamically select a port - public static final int DEFAULT_SERVER_PORT = 0; - - // the default is to have HTTPS disabled - public static final boolean DEFAULT_HTTPS = false; - - // the default is to not use self-signed certificates - public static final boolean DEFAULT_SELF_SIGNED_CERTIFICATES = false; - - // list of remote server names to use in HTTP/HTTPS messages - // (instead of host names) - public static final String HOST_LIST = "server.pool.server.hostlist"; - - // 'Server' timeouts - public static final String SERVER_INITIAL_ALLOWED_GAP = "server.pool.server.allowedGap"; - public static final String SERVER_ADAPTIVE_GAP_ADJUST = - "server.adaptiveGapAdjust"; - public static final String SERVER_CONNECT_TIMEOUT = "server.pool.server.connectTimeout"; - public static final String SERVER_READ_TIMEOUT = "server.pool.server.readTimeout"; - - // at startup, initially allow 30 seconds between pings - public static final long DEFAULT_SERVER_INITIAL_ALLOWED_GAP = 30000; - - // when doing the adaptive calculation of the allowed gap between pings, - // adjust the time by adding 5 seconds (by default) - public static final long DEFAULT_SERVER_ADAPTIVE_GAP_ADJUST = 5000; - - // the default is to allow 10 seconds for a TCP connect - public static final long DEFAULT_SERVER_CONNECT_TIMEOUT = 10000; - - // the default is to allow 10 seconds for a TCP read response - public static final long DEFAULT_SERVER_READ_TIMEOUT = 10000; - - // outgoing per-server thread pool parameters - public static final String SERVER_THREADS_CORE_POOL_SIZE = - "server.pool.server.threads.corePoolSize"; - public static final String SERVER_THREADS_MAXIMUM_POOL_SIZE = - "server.pool.server.threads.maximumPoolSize"; - public static final String SERVER_THREADS_KEEP_ALIVE_TIME = - "server.pool.server.threads.keepAliveTime"; - - public static final int DEFAULT_SERVER_THREADS_CORE_POOL_SIZE = 5; - public static final int DEFAULT_SERVER_THREADS_MAXIMUM_POOL_SIZE = 10; - public static final long DEFAULT_SERVER_THREADS_KEEP_ALIVE_TIME = 5000; - - /*================*/ - /* Host Discovery */ - /*================*/ - - public static final String DISCOVERY_SERVERS = "server.pool.discovery.servers"; - public static final String DISCOVERY_TOPIC = "server.pool.discovery.topic"; - - // HTTP authentication - public static final String DISCOVERY_USERNAME = "server.pool.discovery.username"; - public static final String DISCOVERY_PASSWORD = "server.pool.discovery.password"; - - // Cambria authentication - public static final String DISCOVERY_API_KEY = "server.pool.discovery.apiKey"; - public static final String DISCOVERY_API_SECRET = "server.pool.discovery.apiSecret"; - - // timeouts - public static final String DISCOVERY_FETCH_TIMEOUT = - "server.pool.discovery.fetchTimeout"; - - // this value is passed to the UEB/DMAAP server, and controls how long - // a 'fetch' request will wait when there are no incoming messages - public static final String DEFAULT_DISCOVERY_FETCH_TIMEOUT = "60000"; - - // maximum message fetch limit - public static final String DISCOVERY_FETCH_LIMIT = "server.pool.discovery.fetchLimit"; - - // this value is passed to the UEB/DMAAP server, and controls how many - // requests may be returned in a single fetch - public static final String DEFAULT_DISCOVERY_FETCH_LIMIT = "100"; - - // publisher thread cycle time - public static final String DISCOVER_PUBLISHER_LOOP_CYCLE_TIME = - "discovery.publisherLoopCycleTime"; - - // default cycle time is 5 seconds - public static final long DEFAULT_DISCOVER_PUBLISHER_LOOP_CYCLE_TIME = 5000; - - // encryption - public static final String DISCOVERY_HTTPS = "server.pool.discovery.https"; - public static final String DISCOVERY_ALLOW_SELF_SIGNED_CERTIFICATES = - "server.pool.discovery.selfSignedCertificates"; - - /*============================*/ - /* Leader Election Parameters */ - /*============================*/ - - public static final String LEADER_STABLE_IDLE_CYCLES = - "server.pool.leader.stableIdleCycles"; - public static final String LEADER_STABLE_VOTING_CYCLES = - "server.pool.leader.stableVotingCycles"; - - // by default, wait for 5 cycles (seconds) of stability before voting starts - public static final int DEFAULT_LEADER_STABLE_IDLE_CYCLES = 5; - - // by default, wait for 5 cycles of stability before declaring a winner - public static final int DEFAULT_LEADER_STABLE_VOTING_CYCLES = 5; - - /*=====================*/ - /* MainLoop Parameters */ - /*=====================*/ - - public static final String MAINLOOP_CYCLE = "server.pool.mainLoop.cycle"; - - // by default, the main loop cycle is 1 second - public static final long DEFAULT_MAINLOOP_CYCLE = 1000; - - /*=============================*/ - /* Bucket Migration Parameters */ - /*=============================*/ - - // time-to-live controls how many hops a 'TargetLock' message can take - public static final String BUCKET_TIME_TO_LIVE = "bucket.ttl"; - - // bucket migration timeout when a server has been notified that it - // is the new owner of the bucket - public static final String BUCKET_CONFIRMED_TIMEOUT = - "bucket.confirmed.timeout"; - - // bucket migration timeout when a server has inferred that it may be - // the new owner, but it hasn't yet been confirmed - public static final String BUCKET_UNCONFIRMED_TIMEOUT = - "bucket.unconfirmed.timeout"; - - // timeout for operation run within a Drools session - public static final String BUCKET_DROOLS_TIMEOUT = - "bucket.drools.timeout"; - - // when a new owner of a bucket has completed the takeover of the - // bucket, but it hasn't yet been confirmed, there is an additional - // grace period before leaving the 'NewOwner' state - public static final String BUCKET_UNCONFIRMED_GRACE_PERIOD = - "bucket.unconfirmed.graceperiod"; - - // time-to-live = 5 hops - public static final int DEFAULT_BUCKET_TIME_TO_LIVE = 5; - - // 30 seconds timeout if it has been confirmed that we are the new owner - public static final long DEFAULT_BUCKET_CONFIRMED_TIMEOUT = 30000; - - // 10 seconds timeout if it has not been confirmed that we are the new owner - public static final long DEFAULT_BUCKET_UNCONFIRMED_TIMEOUT = 10000; - - // 10 seconds timeout waiting for a drools operation to complete - public static final long DEFAULT_BUCKET_DROOLS_TIMEOUT = 10000; - - // 10 seconds timeout waiting to be confirmed that we are the new owner - public static final long DEFAULT_BUCKET_UNCONFIRMED_GRACE_PERIOD = 10000; - - /*=======================*/ - /* TargetLock Parameters */ - /*=======================*/ - - // time-to-live controls how many hops a 'TargetLock' message can take - public static final String LOCK_TIME_TO_LIVE = "lock.ttl"; - - // how frequently should the audit run? - public static final String LOCK_AUDIT_PERIOD = "lock.audit.period"; - - // when the audit is rescheduled (e.g. due to a new server joining), this - // is the initial grace period, to allow time for bucket assignments, etc. - public static final String LOCK_AUDIT_GRACE_PERIOD = - "lock.audit.gracePeriod"; - - // there may be audit mismatches detected that are only due to the transient - // nature of the lock state -- we check the mismatches on both sides after - // this delay to see if we are still out-of-sync - public static final String LOCK_AUDIT_RETRY_DELAY = "lock.audit.retryDelay"; - - // time-to-live = 5 hops - public static final int DEFAULT_LOCK_TIME_TO_LIVE = 5; - - // run the audit every 5 minutes - public static final long DEFAULT_LOCK_AUDIT_PERIOD = 300000; - - // wait at least 60 seconds after an event before running the audit - public static final long DEFAULT_LOCK_AUDIT_GRACE_PERIOD = 60000; - - // wait 5 seconds to see if the mismatches still exist - public static final long DEFAULT_LOCK_AUDIT_RETRY_DELAY = 5000; - - /* ============================================================ */ - - private static Logger logger = - LoggerFactory.getLogger(ServerPoolProperties.class); - - // save initial set of properties - private static Properties properties = new Properties(); - - /** - * Hide implicit public constructor. - */ - private ServerPoolProperties() { - // everything here is static -- no instances of this class are created - } - - /** - * Store the application properties values. - * - * @param properties the properties to save - */ - public static void setProperties(Properties properties) { - ServerPoolProperties.properties = properties; - } - - /** - * Return the properties used when starting this server. - * - * @return the properties used when starting this server. - */ - public static Properties getProperties() { - return properties; - } - - /** - * Convenience method to fetch a 'long' property. - * - * @param name the property name - * @param defaultValue the value to use if the property is not defined, - * or has an illegal value - * @return the property value - */ - public static long getProperty(String name, long defaultValue) { - long rval = defaultValue; - String value = properties.getProperty(name); - if (StringUtils.isNotBlank(value)) { - // try to convert to a 'long' -- log a message in case of failure - try { - rval = Long.parseLong(value); - } catch (NumberFormatException e) { - logger.error("Property {}=\"{}\": illegal long -- " - + "using default of {}", name, value, defaultValue); - } - } - return rval; - } - - /** - * Convenience method to fetch an 'int' property. - * - * @param name the property name - * @param defaultValue the value to use if the property is not defined, - * or has an illegal value - * @return the property value - */ - public static int getProperty(String name, int defaultValue) { - int rval = defaultValue; - String value = properties.getProperty(name); - if (StringUtils.isNotBlank(value)) { - // try to convert to an 'int' -- log a message in case of failure - try { - rval = Integer.parseInt(value); - } catch (NumberFormatException e) { - logger.error("Property {}=\"{}\": illegal int -- " - + "using default of {}", name, value, defaultValue); - } - } - return rval; - } - - /** - * Convenience method to fetch a 'boolean' property. - * - * @param name the property name - * @param defaultValue the value to use if the property is not defined, - * or has an illegal value - * @return the property value - */ - public static boolean getProperty(String name, boolean defaultValue) { - boolean rval = defaultValue; - String value = properties.getProperty(name); - if (StringUtils.isNotBlank(value)) { - // try to convert to an 'boolean' -- log a message in case of failure - rval = Boolean.parseBoolean(value); - } - return rval; - } - - /** - * Convenience method to fetch a 'String' property - * (provided for consistency with 'long' and 'int' versions). - * - * @param name the property name - * @param defaultValue the value to use if the property is not defined, - * or has an illegal value - * @return the property value - */ - public static String getProperty(String name, String defaultValue) { - String value = properties.getProperty(name); - return (StringUtils.isNotBlank(value)) ? value : defaultValue; - } -} diff --git a/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/TargetLock.java b/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/TargetLock.java deleted file mode 100644 index f46013ca..00000000 --- a/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/TargetLock.java +++ /dev/null @@ -1,2852 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * feature-server-pool - * ================================================================================ - * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved. - * ================================================================================ - * 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. - * ============LICENSE_END========================================================= - */ - -package org.onap.policy.drools.serverpool; - -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DEFAULT_LOCK_AUDIT_GRACE_PERIOD; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DEFAULT_LOCK_AUDIT_PERIOD; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DEFAULT_LOCK_AUDIT_RETRY_DELAY; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.DEFAULT_LOCK_TIME_TO_LIVE; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.LOCK_AUDIT_GRACE_PERIOD; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.LOCK_AUDIT_PERIOD; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.LOCK_AUDIT_RETRY_DELAY; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.LOCK_TIME_TO_LIVE; -import static org.onap.policy.drools.serverpool.ServerPoolProperties.getProperty; - -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.PrintStream; -import java.io.Serializable; -import java.lang.ref.Reference; -import java.lang.ref.ReferenceQueue; -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.Base64; -import java.util.Collection; -import java.util.Date; -import java.util.HashMap; -import java.util.IdentityHashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Queue; -import java.util.TimerTask; -import java.util.TreeMap; -import java.util.UUID; -import java.util.concurrent.LinkedTransferQueue; -import java.util.concurrent.TimeUnit; -import javax.ws.rs.client.Entity; -import javax.ws.rs.client.WebTarget; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import lombok.EqualsAndHashCode; -import org.onap.policy.drools.core.DroolsRunnable; -import org.onap.policy.drools.core.PolicyContainer; -import org.onap.policy.drools.core.PolicySession; -import org.onap.policy.drools.core.lock.Lock; -import org.onap.policy.drools.core.lock.LockCallback; -import org.onap.policy.drools.core.lock.PolicyResourceLockManager; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * This class provides a locking mechanism based upon a string key that - * identifies the lock, and another string key that identifies the owner. - * The existence of the 'TargetLock' instance doesn't mean that the - * corresponding lock has been acquired -- this is only the case if the - * instance is in the 'ACTIVE' state. - * A lock in the ACTIVE or WAITING state exists in two sets of tables, - * which may be on different hosts: - * LocalLocks - these two tables are associated with the owner key of the - * lock. They are in an adjunct to the bucket associated with this key, - * and the bucket is owned by the host containing the entry. - * GlobalLocks - this table is associated with the lock key. It is an - * adjunct to the bucket associated with this key, and the bucket is - * owned by the host containing the entry. - */ -public class TargetLock implements Lock, Serializable { - private static final String LOCK_MSG = "(key={},owner={},uuid={},ttl={})"; - - private static final long serialVersionUID = 1L; - - private static Logger logger = LoggerFactory.getLogger(TargetLock.class); - - // Listener class to handle state changes that require restarting the audit - private static EventHandler eventHandler = new EventHandler(); - - static { - // register Listener class - Events.register(eventHandler); - } - - // this is used to locate ACTIVE 'TargetLock' instances that have been - // abandoned -- as the GC cleans up the 'WeakReference' instances referring - // to these locks, we use that information to clean them up - private static ReferenceQueue<TargetLock> abandoned = new ReferenceQueue<>(); - - // some status codes - static final int ACCEPTED = 202; //Response.Status.ACCEPTED.getStatusCode() - static final int NO_CONTENT = 204; //Response.Status.NO_CONTENT.getStatusCode() - static final int LOCKED = 423; - - // Values extracted from properties - - private static String timeToLive; - private static long auditPeriod; - private static long auditGracePeriod; - private static long auditRetryDelay; - - // lock states: - // WAITING - in line to acquire the lock - // ACTIVE - currently holding the lock - // FREE - WAITING/ACTIVE locks that were explicitly freed - // LOST - could occur when a de-serialized ACTIVE lock can't be made - // ACTIVE because there is already an ACTIVE holder of the lock - public enum State { - WAITING, ACTIVE, FREE, LOST - } - - // this contains information that is placed in the 'LocalLocks' tables, - // and has a one-to-one correspondence with the 'TargetLock' instance - private Identity identity; - - // this is the only field that can change after initialization - private State state; - - // this is used to notify the application when a lock is available, - // or if it is not available - private final LockCallback owner; - - // This is what is actually called by the infrastructure to do the owner - // notification. The owner may be running in a Drools session, in which case - // the actual notification should be done within that thread -- the 'context' - // object ensures that it happens this way. - private final LockCallback context; - - // HTTP query parameters - private static final String QP_KEY = "key"; - private static final String QP_OWNER = "owner"; - private static final String QP_UUID = "uuid"; - private static final String QP_WAIT = "wait"; - private static final String QP_SERVER = "server"; - private static final String QP_TTL = "ttl"; - - // define a constant for empty of byte array - private static final byte[] EMPTY_BYTE_ARRAY = {}; - - // below are for duplicating string in printout or logger - private static final String PRINTOUT_DASHES = "---------"; - private static final String LOCK_AUDIT = "lock/audit"; - private static final String TARGETLOCK_AUDIT_SEND = "TargetLock.Audit.send: "; - - /** - * This method triggers registration of 'eventHandler', and also extracts - * property values. - */ - static void startup() { - int intTimeToLive = - getProperty(LOCK_TIME_TO_LIVE, DEFAULT_LOCK_TIME_TO_LIVE); - timeToLive = String.valueOf(intTimeToLive); - auditPeriod = getProperty(LOCK_AUDIT_PERIOD, DEFAULT_LOCK_AUDIT_PERIOD); - auditGracePeriod = - getProperty(LOCK_AUDIT_GRACE_PERIOD, DEFAULT_LOCK_AUDIT_GRACE_PERIOD); - auditRetryDelay = - getProperty(LOCK_AUDIT_RETRY_DELAY, DEFAULT_LOCK_AUDIT_RETRY_DELAY); - } - - /** - * Shutdown threads. - */ - static void shutdown() { - AbandonedHandler ah = abandonedHandler; - - if (ah != null) { - abandonedHandler = null; - ah.interrupt(); - } - } - - /** - * Constructor - initializes the 'TargetLock' instance, and tries to go - * ACTIVE. The lock is initially placed in the WAITING state, and the owner - * and the owner will be notified when the success or failure of the lock - * attempt is determined. - * - * @param key string key identifying the lock - * @param ownerKey string key identifying the owner, which must hash to - * a bucket owned by the current host (it is typically a 'RequestID') - * @param owner owner of the lock (will be notified when going from - * WAITING to ACTIVE) - */ - public TargetLock(String key, String ownerKey, LockCallback owner) { - this(key, ownerKey, owner, true); - } - - /** - * Constructor - initializes the 'TargetLock' instance, and tries to go - * ACTIVE. The lock is initially placed in the WAITING state, and the owner - * and the owner will be notified when the success or failure of the lock - * attempt is determined. - * - * @param key string key identifying the lock - * @param ownerKey string key identifying the owner, which must hash to - * a bucket owned by the current host (it is typically a 'RequestID') - * @param owner owner of the lock (will be notified when going from - * WAITING to ACTIVE) - * @param waitForLock this controls the behavior when 'key' is already - * locked - 'true' means wait for it to be freed, 'false' means fail - */ - public TargetLock(final String key, final String ownerKey, - final LockCallback owner, final boolean waitForLock) { - if (key == null) { - throw(new IllegalArgumentException("TargetLock: 'key' can't be null")); - } - if (ownerKey == null) { - throw(new IllegalArgumentException("TargetLock: 'ownerKey' can't be null")); - } - if (!Bucket.isKeyOnThisServer(ownerKey)) { - // associated bucket is assigned to a different server - throw(new IllegalArgumentException("TargetLock: 'ownerKey=" + ownerKey - + "' not currently assigned to this server")); - } - if (owner == null) { - throw(new IllegalArgumentException("TargetLock: 'owner' can't be null")); - } - identity = new Identity(key, ownerKey); - state = State.WAITING; - this.owner = owner; - - // determine the context - PolicySession session = PolicySession.getCurrentSession(); - if (session != null) { - // deliver through a 'PolicySessionContext' class - Object lcontext = session.getAdjunct(PolicySessionContext.class); - if (!(lcontext instanceof LockCallback)) { - context = new PolicySessionContext(session); - session.setAdjunct(PolicySessionContext.class, context); - } else { - context = (LockCallback) lcontext; - } - } else { - // no context to deliver through -- call back directly to owner - context = owner; - } - - // update 'LocalLocks' tables - final WeakReference<TargetLock> wr = new WeakReference<>(this, abandoned); - final LocalLocks localLocks = LocalLocks.get(ownerKey); - - synchronized (localLocks) { - localLocks.weakReferenceToIdentity.put(wr, identity); - localLocks.uuidToWeakReference.put(identity.uuid, wr); - } - - // The associated 'GlobalLocks' table may or may not be on a different - // host. Also, the following call may queue the message for later - // processing if the bucket is in a transient state. - Bucket.forwardAndProcess(key, new Bucket.Message() { - /** - * {@inheritDoc} - */ - @Override - public void process() { - // 'GlobalLocks' is on the same host - State newState = GlobalLocks.get(key).lock(key, ownerKey, identity.uuid, waitForLock); - logger.info("Lock lock request: key={}, owner={}, uuid={}, wait={} (resp={})", - key, ownerKey, identity.uuid, waitForLock, state); - - // The lock may now be ACTIVE, FREE, or WAITING -- we can notify - // the owner of the result now for ACTIVE or FREE. Also, the callback - // may occur while the constructor is still on the stack, although - // this won't happen in a Drools session. - setState(newState); - switch (newState) { - case ACTIVE: - // lock was successful - send notification - context.lockAvailable(TargetLock.this); - break; - case FREE: - // lock attempt failed - - // clean up local tables, and send notification - synchronized (localLocks) { - localLocks.weakReferenceToIdentity.remove(wr); - localLocks.uuidToWeakReference.remove(identity.uuid); - } - wr.clear(); - context.lockUnavailable(TargetLock.this); - break; - - case WAITING: - break; - - default: - logger.error("Unknown state: {}", newState); - break; - } - } - - /** - * {@inheritDoc} - */ - @Override - public void sendToServer(Server server, int bucketNumber) { - // actual lock is on a remote host -- send the request as - // a REST message - logger.info("Sending lock request to {}: key={}, owner={}, uuid={}, wait={}", - server, key, ownerKey, identity.uuid, waitForLock); - server.post("lock/lock", null, new Server.PostResponse() { - /** - * {@inheritDoc} - */ - @Override - public WebTarget webTarget(WebTarget webTarget) { - return webTarget - .queryParam(QP_KEY, key) - .queryParam(QP_OWNER, ownerKey) - .queryParam(QP_UUID, identity.uuid.toString()) - .queryParam(QP_WAIT, waitForLock) - .queryParam(QP_TTL, timeToLive); - } - - /** - * {@inheritDoc} - */ - @Override - public void response(Response response) { - logger.info("Lock response={} (code={})", - response, response.getStatus()); - - /* - * there are three possible responses: - * 204 No Content - operation was successful - * 202 Accepted - operation is still in progress - * 423 (Locked) - lock in use, and 'waitForLock' is 'false' - */ - switch (response.getStatus()) { - case NO_CONTENT: - // lock successful - setState(State.ACTIVE); - context.lockAvailable(TargetLock.this); - break; - - case LOCKED: - // failed -- lock in use, and 'waitForLock == false' - setState(State.FREE); - synchronized (localLocks) { - localLocks.weakReferenceToIdentity.remove(wr); - localLocks.uuidToWeakReference.remove(identity.uuid); - } - wr.clear(); - context.lockUnavailable(TargetLock.this); - break; - - case ACCEPTED: - break; - - default: - logger.error("Unknown status: {}", response.getStatus()); - break; - } - } - }); - } - }); - } - - /* ****************** */ - /* 'Lock' Interface */ - /* ****************** */ - - /** - * This method will free the current lock, or remove it from the waiting - * list if a response is pending. - * - * @return 'true' if successful, 'false' if it was not locked, or there - * appears to be corruption in 'LocalLocks' tables - */ - @Override - public boolean free() { - synchronized (this) { - if (state != State.ACTIVE && state != State.WAITING) { - // nothing to free - return false; - } - state = State.FREE; - } - - return identity.free(); - } - - /** - * Return 'true' if the lock is in the ACTIVE state. - * - * @return 'true' if the lock is in the ACTIVE state, and 'false' if not - */ - @Override - public synchronized boolean isActive() { - return state == State.ACTIVE; - } - - /** - * Return 'true' if the lock is not available. - * - * @return 'true' if the lock is in the FREE or LOST state, - * and 'false' if not - */ - @Override - public synchronized boolean isUnavailable() { - return state == State.FREE || state == State.LOST; - } - - /** - * Return 'true' if the lock is in the WAITING state. - * - * @return 'true' if the lock is in the WAITING state, and 'false' if not - */ - public synchronized boolean isWaiting() { - return state == State.WAITING; - } - - /** - * Return the lock's key. - * - * @return the lock's key - */ - @Override - public String getResourceId() { - return identity.key; - } - - /** - * Return the owner key field. - * - * @return the owner key field - */ - @Override - public String getOwnerKey() { - return identity.ownerKey; - } - - /** - * Extends the lock's hold time (not implemented yet). - */ - @Override - public void extend(int holdSec, LockCallback callback) { - // not implemented yet - } - - /* ****************** */ - - /** - * Update the state. - * - * @param newState the new state value - */ - private synchronized void setState(State newState) { - state = newState; - } - - /** - * Return the currentstate of the lock. - * - * @return the current state of the lock - */ - public synchronized State getState() { - return state; - } - - /** - * This method is called when an incoming /lock/lock REST message is received. - * - * @param key string key identifying the lock, which must hash to a bucket - * owned by the current host - * @param ownerKey string key identifying the owner - * @param uuid the UUID that uniquely identifies the original 'TargetLock' - * @param waitForLock this controls the behavior when 'key' is already - * locked - 'true' means wait for it to be freed, 'false' means fail - * @param ttl similar to IP time-to-live -- it controls the number of hops - * the message may take - * @return the Response that should be passed back to the HTTP request - */ - static Response incomingLock(String key, String ownerKey, UUID uuid, boolean waitForLock, int ttl) { - if (!Bucket.isKeyOnThisServer(key)) { - // this is the wrong server -- forward to the correct one - // (we can use this thread) - ttl -= 1; - if (ttl > 0) { - Server server = Bucket.bucketToServer(Bucket.bucketNumber(key)); - if (server != null) { - WebTarget webTarget = server.getWebTarget("lock/lock"); - if (webTarget != null) { - logger.warn("Forwarding 'lock/lock' to uuid {} " - + "(key={},owner={},uuid={},wait={},ttl={})", - server.getUuid(), key, ownerKey, uuid, - waitForLock, ttl); - return webTarget - .queryParam(QP_KEY, key) - .queryParam(QP_OWNER, ownerKey) - .queryParam(QP_UUID, uuid.toString()) - .queryParam(QP_WAIT, waitForLock) - .queryParam(QP_TTL, String.valueOf(ttl)) - .request().get(); - } - } - } - - // if we reach this point, we didn't forward for some reason -- - // return failure by indicating it is locked and unavailable - logger.error("Couldn't forward 'lock/lock' " - + "(key={},owner={},uuid={},wait={},ttl={})", - key, ownerKey, uuid, waitForLock, ttl); - return Response.noContent().status(LOCKED).build(); - } - - State state = GlobalLocks.get(key).lock(key, ownerKey, uuid, waitForLock); - switch (state) { - case ACTIVE: - return Response.noContent().build(); - case WAITING: - return Response.noContent().status(Response.Status.ACCEPTED).build(); - default: - return Response.noContent().status(LOCKED).build(); - } - } - - /** - * This method is called when an incoming /lock/free REST message is received. - * - * @param key string key identifying the lock, which must hash to a bucket - * owned by the current host - * @param ownerKey string key identifying the owner - * @param uuid the UUID that uniquely identifies the original 'TargetLock' - * @param ttl similar to IP time-to-live -- it controls the number of hops - * the message may take - * @return the Response that should be passed back to the HTTP request - */ - static Response incomingFree(String key, String ownerKey, UUID uuid, int ttl) { - if (!Bucket.isKeyOnThisServer(key)) { - // this is the wrong server -- forward to the correct one - // (we can use this thread) - ttl -= 1; - if (ttl > 0) { - Server server = Bucket.bucketToServer(Bucket.bucketNumber(key)); - if (server != null) { - WebTarget webTarget = server.getWebTarget("lock/free"); - if (webTarget != null) { - logger.warn("Forwarding 'lock/free' to uuid {} " - + LOCK_MSG, - server.getUuid(), key, ownerKey, uuid, ttl); - return webTarget - .queryParam(QP_KEY, key) - .queryParam(QP_OWNER, ownerKey) - .queryParam(QP_UUID, uuid.toString()) - .queryParam(QP_TTL, String.valueOf(ttl)) - .request().get(); - } - } - } - - // if we reach this point, we didn't forward for some reason -- - // return failure by indicating it is locked and unavailable - logger.error("Couldn't forward 'lock/free' " - + LOCK_MSG, - key, ownerKey, uuid, ttl); - return null; - } - - // TBD: should this return a more meaningful response? - GlobalLocks.get(key).unlock(key, uuid); - return null; - } - - /** - * This method is called when an incoming /lock/locked message is received - * (this is a callback to an earlier requestor that the lock is now - * available). - * - * @param key string key identifying the lock - * @param ownerKey string key identifying the owner, which must hash to - * a bucket owned by the current host (it is typically a 'RequestID') - * @param uuid the UUID that uniquely identifies the original 'TargetLock' - * @param ttl similar to IP time-to-live -- it controls the number of hops - * the message may take - * @return the Response that should be passed back to the HTTP request - */ - static Response incomingLocked(String key, String ownerKey, UUID uuid, int ttl) { - if (!Bucket.isKeyOnThisServer(ownerKey)) { - // this is the wrong server -- forward to the correct one - // (we can use this thread) - ttl -= 1; - if (ttl > 0) { - Server server = Bucket.bucketToServer(Bucket.bucketNumber(key)); - if (server != null) { - WebTarget webTarget = server.getWebTarget("lock/locked"); - if (webTarget != null) { - logger.warn("Forwarding 'lock/locked' to uuid {} " - + LOCK_MSG, - server.getUuid(), key, ownerKey, uuid, ttl); - return webTarget - .queryParam(QP_KEY, key) - .queryParam(QP_OWNER, ownerKey) - .queryParam(QP_UUID, uuid.toString()) - .queryParam(QP_TTL, String.valueOf(ttl)) - .request().get(); - } - } - } - - // if we reach this point, we didn't forward for some reason -- - // return failure by indicating it is locked and unavailable - logger.error("Couldn't forward 'lock/locked' " - + LOCK_MSG, - key, ownerKey, uuid, ttl); - return Response.noContent().status(LOCKED).build(); - } - - TargetLock targetLock; - LocalLocks localLocks = LocalLocks.get(ownerKey); - synchronized (localLocks) { - targetLock = grabLock(uuid, localLocks); - } - if (targetLock == null) { - // We can't locate the target lock - // TBD: This probably isn't the best error code to use - return Response.noContent().status(LOCKED).build(); - } else { - targetLock.context.lockAvailable(targetLock); - return Response.noContent().build(); - } - } - - private static TargetLock grabLock(UUID uuid, LocalLocks localLocks) { - WeakReference<TargetLock> wr = - localLocks.uuidToWeakReference.get(uuid); - - if (wr != null) { - TargetLock targetLock = wr.get(); - if (targetLock == null) { - // lock has been abandoned - // (AbandonedHandler should usually find this first) - localLocks.weakReferenceToIdentity.remove(wr); - localLocks.uuidToWeakReference.remove(uuid); - } else { - // the lock has been made available -- update the state - // TBD: This could be outside of 'synchronized (localLocks)' - synchronized (targetLock) { - if (targetLock.state == State.WAITING) { - targetLock.state = State.ACTIVE; - return targetLock; - } else { - // will return a failure -- not sure how this happened - logger.error("incomingLocked: {} is in state {}", - targetLock, targetLock.state); - } - } - } - } else { - // clean up what we can - localLocks.uuidToWeakReference.remove(uuid); - } - - return null; - } - - /** - * This is called when the state of a bucket has changed, but is currently - * stable. Note that this method is called while being synchronized on the - * bucket. - * - * @param bucket the bucket to audit - * @param owner 'true' if the current host owns the bucket - */ - static void auditBucket(Bucket bucket, boolean isOwner) { - if (!isOwner) { - // we should not have any 'TargetLock' adjuncts - if (bucket.removeAdjunct(LocalLocks.class) != null) { - logger.warn("Bucket {}: Removed superfluous " - + "'TargetLock.LocalLocks' adjunct", - bucket.getIndex()); - } - if (bucket.removeAdjunct(GlobalLocks.class) != null) { - logger.warn("Bucket {}: Removed superfluous " - + "'TargetLock.GlobalLocks' adjunct", - bucket.getIndex()); - } - } - } - - /** - * {@inheritDoc} - */ - @Override - public String toString() { - return "TargetLock(key=" + identity.key - + ", ownerKey=" + identity.ownerKey - + ", uuid=" + identity.uuid - + ", state=" + state + ")"; - } - - /* *************** */ - /* Serialization */ - /* *************** */ - - /** - * This method modifies the behavior of 'TargetLock' deserialization by - * creating the corresponding 'LocalLocks' entries. - */ - private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { - in.defaultReadObject(); - if (state == State.ACTIVE || state == State.WAITING) { - // need to build entries in 'LocalLocks' - LocalLocks localLocks = LocalLocks.get(identity.ownerKey); - WeakReference<TargetLock> wr = new WeakReference<>(this, abandoned); - - synchronized (localLocks) { - localLocks.weakReferenceToIdentity.put(wr, identity); - localLocks.uuidToWeakReference.put(identity.uuid, wr); - } - } - } - - /* ============================================================ */ - - private static class LockFactory implements PolicyResourceLockManager { - /* *************************************** */ - /* 'PolicyResourceLockManager' interface */ - /* *************************************** */ - - /** - * {@inheritDoc} - */ - @Override - public Lock createLock(String resourceId, String ownerKey, - int holdSec, LockCallback callback, - boolean waitForLock) { - // 'holdSec' isn't implemented yet - return new TargetLock(resourceId, ownerKey, callback, waitForLock); - } - - /* *********************** */ - /* 'Startable' interface */ - /* *********************** */ - - /** - * {@inheritDoc} - */ - @Override - public boolean start() { - return true; - } - - /** - * {@inheritDoc} - */ - @Override - public boolean stop() { - return false; - } - - /** - * {@inheritDoc} - */ - @Override - public void shutdown() { - // nothing needs to be done - } - - /** - * {@inheritDoc} - */ - @Override - public boolean isAlive() { - return true; - } - - /* ********************** */ - /* 'Lockable' interface */ - /* ********************** */ - - /** - * {@inheritDoc} - */ - @Override - public boolean lock() { - return false; - } - - /** - * {@inheritDoc} - */ - @Override - public boolean unlock() { - return true; - } - - /** - * {@inheritDoc} - */ - @Override - public boolean isLocked() { - return false; - } - } - - private static LockFactory lockFactory = new LockFactory(); - - public static PolicyResourceLockManager getLockFactory() { - return lockFactory; - } - - /* ============================================================ */ - - /** - * There is a single instance of class 'TargetLock.EventHandler', which is - * registered to listen for notifications of state transitions. - */ - private static class EventHandler extends Events { - /** - * {@inheritDoc} - */ - @Override - public void newServer(Server server) { - // with an additional server, the offset within the audit period changes - Audit.scheduleAudit(); - } - - /** - * {@inheritDoc} - */ - @Override - public void serverFailed(Server server) { - // when one less server, the offset within the audit period changes - Audit.scheduleAudit(); - } - } - - /* ============================================================ */ - - /** - * This class usually has a one-to-one correspondence with a 'TargetLock' - * instance, unless the 'TargetLock' has been abandoned. - */ - @EqualsAndHashCode - private static class Identity implements Serializable { - private static final long serialVersionUID = 1L; - - // this is the key associated with the lock - String key; - - // this is the key associated with the lock requestor - String ownerKey; - - // this is a unique identifier assigned to the 'TargetLock' - UUID uuid; - - /** - * Constructor - initializes the 'Identity' instance, including the - * generation of the unique identifier. - * - * @param key string key identifying the lock - * @param ownerKey string key identifying the owner, which must hash to - * a bucket owned by the current host (it is typically a 'RequestID') - */ - private Identity(String key, String ownerKey) { - this.key = key; - this.ownerKey = ownerKey; - this.uuid = UUID.randomUUID(); - } - - /** - * Constructor - initializes the 'Identity' instance, with the 'uuid' - * value passed at initialization time (only used for auditing). - * - * @param key string key identifying the lock - * @param ownerKey string key identifying the owner, which must hash to - * @param uuid the UUID that uniquely identifies the original 'TargetLock' - */ - private Identity(String key, String ownerKey, UUID uuid) { - this.key = key; - this.ownerKey = ownerKey; - this.uuid = uuid; - } - - /** - * Free the lock associated with this 'Identity' instance. - * - * @return 'false' if the 'LocalLocks' data is not there, true' if it is - */ - private boolean free() { - // free the lock - Bucket.forwardAndProcess(key, new Bucket.Message() { - /** - * {@inheritDoc} - */ - @Override - public void process() { - // the global lock entry is also on this server - GlobalLocks.get(key).unlock(key, uuid); - } - - /** - * {@inheritDoc} - */ - @Override - public void sendToServer(Server server, int bucketNumber) { - logger.info("Sending free request to {}: key={}, owner={}, uuid={}", - server, key, ownerKey, uuid); - server.post("lock/free", null, new Server.PostResponse() { - @Override - public WebTarget webTarget(WebTarget webTarget) { - return webTarget - .queryParam(QP_KEY, key) - .queryParam(QP_OWNER, ownerKey) - .queryParam(QP_UUID, uuid.toString()) - .queryParam(QP_TTL, timeToLive); - } - - @Override - public void response(Response response) { - logger.info("Free response={} (code={})", - response, response.getStatus()); - switch (response.getStatus()) { - case NO_CONTENT: - // free successful -- don't need to do anything - break; - - case LOCKED: - // free failed - logger.error("TargetLock free failed, " - + "key={}, owner={}, uuid={}", - key, ownerKey, uuid); - break; - - default: - logger.error("Unknown status: {}", response.getStatus()); - break; - } - } - }); - } - }); - - // clean up locallocks entry - LocalLocks localLocks = LocalLocks.get(ownerKey); - synchronized (localLocks) { - WeakReference<TargetLock> wr = - localLocks.uuidToWeakReference.get(uuid); - if (wr == null) { - return false; - } - - localLocks.weakReferenceToIdentity.remove(wr); - localLocks.uuidToWeakReference.remove(uuid); - wr.clear(); - } - return true; - } - } - - /* ============================================================ */ - - /** - * An instance of this class is used for 'TargetLock.context' when the - * lock is allocated within a Drools session. Its purpose is to ensure that - * the callback to 'TargetLock.owner' runs within the Drools thread. - */ - private static class PolicySessionContext implements LockCallback, Serializable { - private static final long serialVersionUID = 1L; - - // the 'PolicySession' instance in question - PolicySession policySession; - - /** - * Constructor - initialize the 'policySession' field. - * - * @param policySession the Drools session - */ - private PolicySessionContext(PolicySession policySession) { - this.policySession = policySession; - } - - /* ******************* */ - /* 'Owner' interface */ - /* ******************* */ - - /** - * {@inheritDoc} - */ - @Override - public void lockAvailable(final Lock lock) { - // Run 'owner.lockAvailable' within the Drools session - if (policySession != null) { - DroolsRunnable callback = () -> - ((TargetLock) lock).owner.lockAvailable(lock); - policySession.getKieSession().insert(callback); - } - } - - /** - * {@inheritDoc} - */ - @Override - public void lockUnavailable(Lock lock) { - // Run 'owner.unlockAvailable' within the Drools session - if (policySession != null) { - DroolsRunnable callback = () -> - ((TargetLock) lock).owner.lockUnavailable(lock); - policySession.getKieSession().insert(callback); - } - } - - /* *************** */ - /* Serialization */ - /* *************** */ - - /** - * Specializes serialization of 'PolicySessionContext'. - */ - private void writeObject(ObjectOutputStream out) throws IOException { - // 'PolicySession' can't be serialized directly -- - // store as 'groupId', 'artifactId', 'sessionName' - PolicyContainer pc = policySession.getPolicyContainer(); - - out.writeObject(pc.getGroupId()); - out.writeObject(pc.getArtifactId()); - out.writeObject(policySession.getName()); - } - - /** - * Specializes deserialization of 'PolicySessionContext'. - */ - private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { - // 'PolicySession' can't be serialized directly -- - // read in 'groupId', 'artifactId', 'sessionName' - String groupId = String.class.cast(in.readObject()); - String artifactId = String.class.cast(in.readObject()); - String sessionName = String.class.cast(in.readObject()); - - // locate the 'PolicySession' associated with - // 'groupId', 'artifactId', and 'sessionName' - for (PolicyContainer pc : PolicyContainer.getPolicyContainers()) { - if (artifactId.equals(pc.getArtifactId()) - && groupId.equals(pc.getGroupId())) { - // found 'PolicyContainer' -- look up the session - policySession = pc.getPolicySession(sessionName); - if (policySession == null) { - logger.error("TargetLock.PolicySessionContext.readObject: " - + "Can't find session {}:{}:{}", - groupId, artifactId, sessionName); - } - } - } - } - } - - /* ============================================================ */ - - /** - * This class contains two tables that have entries for any 'TargetLock' - * in the 'ACTIVE' or 'WAITING' state. This is the "client" end of the - * lock implementation. - */ - static class LocalLocks { - // this table makes it easier to clean up locks that have been - // abandoned (see 'AbandonedHandler') - private Map<WeakReference<TargetLock>, Identity> weakReferenceToIdentity = new IdentityHashMap<>(); - - // this table is used to locate a 'TargetLock' instance from a UUID - private Map<UUID, WeakReference<TargetLock>> uuidToWeakReference = - new HashMap<>(); - - /** - * Fetch the 'LocalLocks' entry associated with a particular owner key - * (it is created if necessary). - * - * @param ownerKey string key identifying the owner, which must hash to - * a bucket owned by the current host (it is typically a 'RequestID') - * @return the associated 'LocalLocks' instance (it should never be 'null') - */ - private static LocalLocks get(String ownerKey) { - return Bucket.getBucket(ownerKey).getAdjunct(LocalLocks.class); - } - } - - /* ============================================================ */ - - /** - * This class contains the actual lock table, which is the "server" end - * of the lock implementation. - */ - public static class GlobalLocks implements Serializable { - private static final long serialVersionUID = 1L; - - // this is the lock table, mapping 'key' to 'LockEntry', which indicates - // the current lock holder, and all those waiting - private Map<String, LockEntry> keyToEntry = new HashMap<>(); - - /** - * Fetch the 'GlobalLocks' entry associated with a particular key - * (it is created if necessary). - * - * @param key string key identifying the lock - * @return the associated 'GlobalLocks' instance - * (it should never be 'null') - */ - private static GlobalLocks get(String key) { - return Bucket.getBucket(key).getAdjunct(GlobalLocks.class); - } - - /** - * Do the 'lock' operation -- lock immediately, if possible. If not, - * get on the waiting list, if requested. - * - * @param key string key identifying the lock, which must hash to a bucket - * owned by the current host - * @param ownerKey string key identifying the owner - * @param uuid the UUID that uniquely identifies the original 'TargetLock' - * (on the originating host) - * @param waitForLock this controls the behavior when 'key' is already - * locked - 'true' means wait for it to be freed, 'false' means fail - * @return the lock State corresponding to the current request - */ - synchronized State lock(String key, String ownerKey, UUID uuid, boolean waitForLock) { - synchronized (keyToEntry) { - LockEntry entry = keyToEntry.get(key); - if (entry == null) { - // there is no existing entry -- create one, and return ACTIVE - entry = new LockEntry(key, ownerKey, uuid); - keyToEntry.put(key, entry); - sendUpdate(key); - return State.ACTIVE; - } - if (waitForLock) { - // the requestor is willing to wait -- get on the waiting list, - // and return WAITING - entry.waitingList.add(new Waiting(ownerKey, uuid)); - sendUpdate(key); - return State.WAITING; - } - - // the requestor is not willing to wait -- return FREE, - // which will be interpreted as a failure - return State.FREE; - } - } - - /** - * Free a lock or a pending lock request. - * - * @param key string key identifying the lock - * @param uuid the UUID that uniquely identifies the original 'TargetLock' - */ - synchronized void unlock(String key, UUID uuid) { - synchronized (keyToEntry) { - final LockEntry entry = keyToEntry.get(key); - if (entry == null) { - logger.error("GlobalLocks.unlock: unknown lock, key={}, uuid={}", - key, uuid); - return; - } - if (entry.currentOwnerUuid.equals(uuid)) { - // this is the current lock holder - if (entry.waitingList.isEmpty()) { - // free this lock - keyToEntry.remove(key); - } else { - // pass it on to the next one in the list - Waiting waiting = entry.waitingList.remove(); - entry.currentOwnerKey = waiting.ownerKey; - entry.currentOwnerUuid = waiting.ownerUuid; - - entry.notifyNewOwner(this); - } - sendUpdate(key); - } else { - // see if one of the waiting entries is being freed - for (Waiting waiting : entry.waitingList) { - if (waiting.ownerUuid.equals(uuid)) { - entry.waitingList.remove(waiting); - sendUpdate(key); - break; - } - } - } - } - } - - /** - * Notify all features that an update has occurred on this GlobalLock. - * - * @param key the key associated with the change - * (used to locate the bucket) - */ - private void sendUpdate(String key) { - Bucket bucket = Bucket.getBucket(key); - for (ServerPoolApi feature : ServerPoolApi.impl.getList()) { - feature.lockUpdate(bucket, this); - } - } - - /*===============*/ - /* Serialization */ - /*===============*/ - - private void writeObject(ObjectOutputStream out) throws IOException { - synchronized (this) { - out.defaultWriteObject(); - } - } - } - - /* ============================================================ */ - - /** - * Each instance of this object corresponds to a single key in the lock - * table. It includes the current holder of the lock, as well as - * any that are waiting. - */ - private static class LockEntry implements Serializable { - private static final long serialVersionUID = 1L; - - // string key identifying the lock - private String key; - - // string key identifying the owner - private String currentOwnerKey; - - // UUID identifying the original 'TargetLock - private UUID currentOwnerUuid; - - // list of pending lock requests for this key - private Queue<Waiting> waitingList = new LinkedList<>(); - - /** - * Constructor - initialize the 'LockEntry'. - * - * @param key string key identifying the lock, which must hash to a bucket - * owned by the current host - * @param ownerKey string key identifying the owner - * @param uuid the UUID that uniquely identifies the original 'TargetLock' - */ - private LockEntry(String key, String ownerKey, UUID uuid) { - this.key = key; - this.currentOwnerKey = ownerKey; - this.currentOwnerUuid = uuid; - } - - /** - * This method is called after the 'currentOwnerKey' and - * 'currentOwnerUuid' fields have been updated, and it notifies the new - * owner that they now have the lock. - * - * @param globalLocks the 'GlobalLocks' instance containing this entry - */ - private void notifyNewOwner(final GlobalLocks globalLocks) { - Bucket.forwardAndProcess(currentOwnerKey, new Bucket.Message() { - /** - * {@inheritDoc} - */ - @Override - public void process() { - // the new owner is on this host - incomingLocked(key, currentOwnerKey, currentOwnerUuid, 1); - } - - /** - * {@inheritDoc} - */ - @Override - public void sendToServer(Server server, int bucketNumber) { - // the new owner is on a remote host - logger.info("Sending locked notification to {}: key={}, owner={}, uuid={}", - server, key, currentOwnerKey, currentOwnerUuid); - server.post("lock/locked", null, new Server.PostResponse() { - @Override - public WebTarget webTarget(WebTarget webTarget) { - return webTarget - .queryParam(QP_KEY, key) - .queryParam(QP_OWNER, currentOwnerKey) - .queryParam(QP_UUID, currentOwnerUuid.toString()) - .queryParam(QP_TTL, timeToLive); - } - - @Override - public void response(Response response) { - logger.info("Locked response={} (code={})", - response, response.getStatus()); - if (response.getStatus() != NO_CONTENT) { - // notification failed -- free this one - globalLocks.unlock(key, currentOwnerUuid); - } - } - }); - } - }); - - } - } - - /* ============================================================ */ - - /** - * This corresponds to a member of 'LockEntry.waitingList' - */ - private static class Waiting implements Serializable { - private static final long serialVersionUID = 1L; - - // string key identifying the owner - String ownerKey; - - // uniquely identifies the new owner 'TargetLock' - UUID ownerUuid; - - /** - * Constructor. - * - * @param ownerKey string key identifying the owner - * @param ownerUuid uniquely identifies the new owner 'TargetLock' - */ - private Waiting(String ownerKey, UUID ownerUuid) { - this.ownerKey = ownerKey; - this.ownerUuid = ownerUuid; - } - } - - /* ============================================================ */ - - /** - * Backup data associated with a 'GlobalLocks' instance. - */ - static class LockBackup implements Bucket.Backup { - /** - * {@inheritDoc} - */ - @Override - public Bucket.Restore generate(int bucketNumber) { - Bucket bucket = Bucket.getBucket(bucketNumber); - - // just remove 'LocalLocks' -- it will need to be rebuilt from - // 'TargetLock' instances - bucket.removeAdjunct(LocalLocks.class); - - // global locks need to be transferred - GlobalLocks globalLocks = bucket.removeAdjunct(GlobalLocks.class); - return globalLocks == null ? null : new LockRestore(globalLocks); - } - } - - /* ============================================================ */ - - /** - * This class is used to restore a 'GlobalLocks' instance from a backup. - */ - static class LockRestore implements Bucket.Restore, Serializable { - private static final long serialVersionUID = 1L; - - GlobalLocks globalLocks; - - /** - * Constructor - runs as part of backup (deserialization bypasses this constructor). - * - * @param globalLocks GlobalLocks instance extracted as part of backup - */ - LockRestore(GlobalLocks globalLocks) { - this.globalLocks = globalLocks; - } - - /** - * {@inheritDoc} - */ - @Override - public void restore(int bucketNumber) { - // fetch bucket - Bucket bucket = Bucket.getBucket(bucketNumber); - - // update the adjunct - if (bucket.putAdjunct(globalLocks) != null) { - logger.error("LockRestore({}): GlobalLocks adjunct already existed", - bucketNumber); - } - - // notify features of the 'globalLocks' update - for (ServerPoolApi feature : ServerPoolApi.impl.getList()) { - feature.lockUpdate(bucket, globalLocks); - } - } - } - - /* ============================================================ */ - - /** - * This class is a deamon that monitors the 'abandoned' queue. If an - * ACTIVE 'TargetLock' is abandoned, the GC will eventually place the - * now-empty 'WeakReference' in this queue. - */ - private static class AbandonedHandler extends Thread { - AbandonedHandler() { - super("TargetLock.AbandonedHandler"); - } - - /** - * This method camps on the 'abandoned' queue, processing entries as - * they are received. - */ - @Override - public void run() { - while (abandonedHandler != null) { - try { - Reference<? extends TargetLock> wr = abandoned.remove(); - - // At this point, we know that 'ref' is a - // 'WeakReference<TargetLock>' instance that has been abandoned, - // but we don't know what the associated 'Identity' instance - // is. Here, we search through every bucket looking for a - // matching entry. The assumption is that this is rare enough, - // and due to a bug, so it doesn't hurt to spend extra CPU time - // here. The alternative is to add some additional information - // to make this mapping quick, at the expense of a slight - // slow down of normal lock operations. - for (int i = 0; i < Bucket.BUCKETCOUNT; i += 1) { - LocalLocks localLocks = - Bucket.getBucket(i).getAdjunctDontCreate(LocalLocks.class); - if (localLocks != null) { - // the adjunct does exist -- see if the WeakReference - // instance is known to this bucket - synchronized (localLocks) { - Identity identity = - localLocks.weakReferenceToIdentity.get(wr); - if (identity != null) { - // found it - logger.error("Abandoned TargetLock: bucket={}, " - + "key={}, ownerKey={}, uuid={}", - i, identity.key, identity.ownerKey, - identity.uuid); - identity.free(); - break; - } - } - } - } - } catch (Exception e) { - logger.error("TargetLock.AbandonedHandler exception", e); - } - } - } - } - - // create a single instance of 'AbandonedHandler', and start it - private static AbandonedHandler abandonedHandler = new AbandonedHandler(); - - static { - abandonedHandler.start(); - } - - /* ============================================================ */ - - /** - * This class handles the '/cmd/dumpLocks' REST command. - */ - static class DumpLocks { - // indicates whether a more detailed dump should be done - private boolean detail; - - // this table maps the 'TargetLock' UUID into an object containing - // both client (LocalLocks) and server (GlobalLocks) information - private Map<UUID, MergedData> mergedDataMap = - new TreeMap<>(Util.uuidComparator); - - // this table maps the 'TargetLock' key into the associated 'LockEntry' - // (server end) - private Map<String, LockEntry> lockEntries = new TreeMap<>(); - - // this table maps the 'TargetLock' key into entries that only exist - // on the client end - private Map<String, MergedData> clientOnlyEntries = new TreeMap<>(); - - // display format (although it is now dynamically adjusted) - private String format = "%-14s %-14s %-36s %-10s %s\n"; - - // calculation of maximum key length for display - private int keyLength = 10; - - // calculation of maximum owner key length for display - private int ownerKeyLength = 10; - - // 'true' if any comments need to be displayed (affects format) - private boolean commentsIncluded = false; - - /** - * Entry point for the '/cmd/dumpLocks' REST command. - * - * @param out where the output should be displayed - * @param detail 'true' provides additional bucket and host information - * (but abbreviates all UUIDs in order to avoid excessive - * line length) - */ - static void dumpLocks(PrintStream out, boolean detail) - throws InterruptedException, IOException, ClassNotFoundException { - - // the actual work is done in the constructor - new DumpLocks(out, detail); - } - - /** - * Entry point for the '/lock/dumpLocksData' REST command, which generates - * a byte stream for this particular host. - * - * @param serverUuid the UUID of the intended destination server - * @param ttl similar to IP time-to-live -- it controls the number of hops - * the message may take - * @return a base64-encoded byte stream containing serialized 'HostData' - */ - static byte[] dumpLocksData(UUID serverUuid, int ttl) throws IOException { - if (!Server.getThisServer().getUuid().equals(serverUuid)) { - ttl -= 1; - if (ttl > 0) { - Server server = Server.getServer(serverUuid); - if (server != null) { - WebTarget webTarget = - server.getWebTarget("lock/dumpLocksData"); - if (webTarget != null) { - logger.info("Forwarding 'lock/dumpLocksData' to uuid {}", - serverUuid); - return webTarget - .queryParam(QP_SERVER, serverUuid.toString()) - .queryParam(QP_TTL, String.valueOf(ttl)) - .request().get(byte[].class); - } - } - } - - // if we reach this point, we didn't forward for some reason - - logger.error("Couldn't forward 'lock/dumpLocksData to uuid {}", - serverUuid); - return EMPTY_BYTE_ARRAY; - } - - return Base64.getEncoder().encode(Util.serialize(new HostData())); - } - - /** - * Constructor - does the '/cmd/dumpLocks' REST command. - * - * @param out where the output should be displayed - */ - DumpLocks(PrintStream out, boolean detail) - throws IOException, InterruptedException, ClassNotFoundException { - - this.detail = detail; - - // receives responses from '/lock/dumpLocksData' - final LinkedTransferQueue<Response> responseQueue = - new LinkedTransferQueue<>(); - - // generate a count of the number of external servers that should respond - int pendingResponseCount = 0; - - // iterate over all of the servers - for (final Server server : Server.getServers()) { - if (server == Server.getThisServer()) { - // skip this server -- we will handle it differently - continue; - } - - // keep a running count - pendingResponseCount += 1; - server.post("lock/dumpLocksData", null, new Server.PostResponse() { - @Override - public WebTarget webTarget(WebTarget webTarget) { - return webTarget - .queryParam(QP_SERVER, server.getUuid().toString()) - .queryParam(QP_TTL, timeToLive); - } - - @Override - public void response(Response response) { - // responses are queued, and the main thread will collect them - responseQueue.put(response); - } - }); - } - - // this handles data associated with this server -- it also goes through - // serialization/deserialization, which provides a deep copy of the data - populateLockData(dumpLocksData(Server.getThisServer().getUuid(), 0)); - - // now, poll for responses from all of the the other servers - while (pendingResponseCount > 0) { - pendingResponseCount -= 1; - Response response = responseQueue.poll(60, TimeUnit.SECONDS); - if (response == null) { - // timeout -- we aren't expecting any more responses - break; - } - - // populate data associated with this server - populateLockData(response.readEntity(byte[].class)); - } - - // we have processed all of the servers that we are going to, - // now generate the output - dump(out); - } - - /** - * process base64-encoded data from a server (local or remote). - * - * @param data base64-encoded data (class 'HostData') - */ - void populateLockData(byte[] data) throws IOException, ClassNotFoundException { - Object decodedData = Util.deserialize(Base64.getDecoder().decode(data)); - if (decodedData instanceof HostData) { - // deserialized data - HostData hostData = (HostData) decodedData; - - // fetch 'Server' instance associated with the responding server - Server server = Server.getServer(hostData.hostUuid); - - // process the client-end data - for (ClientData clientData : hostData.clientDataList) { - populateLockDataClientData(clientData, server); - } - - // process the server-end data - for (ServerData serverData : hostData.serverDataList) { - populateLockDataServerData(serverData, server); - } - } else { - logger.error("TargetLock.DumpLocks.populateLockData: " - + "received data has class {}", - decodedData.getClass().getName()); - } - } - - private void populateLockDataClientData(ClientData clientData, Server server) { - // 'true' if the bucket associated with this 'ClientData' - // doesn't belong to the remote server, as far as we can tell - boolean serverMismatch = - Bucket.bucketToServer(clientData.bucketNumber) != server; - - // each 'ClientDataRecord' instance corresponds to an - // active 'Identity' (TargetLock) instance - for (ClientDataRecord cdr : clientData.clientDataRecords) { - // update maximum 'key' and 'ownerKey' lengths - updateKeyLength(cdr.identity.key); - updateOwnerKeyLength(cdr.identity.ownerKey); - - // fetch UUID - UUID uuid = cdr.identity.uuid; - - // fetch/generate 'MergeData' instance for this UUID - MergedData md = mergedDataMap.computeIfAbsent(uuid, key -> new MergedData(uuid)); - - // update 'MergedData.clientDataRecord' - if (md.clientDataRecord == null) { - md.clientDataRecord = cdr; - } else { - md.comment("Duplicate client entry for UUID"); - } - - if (serverMismatch) { - // need to generate an additional error - md.comment(server.toString() - + "(client) does not own bucket " - + clientData.bucketNumber); - } - } - } - - private void populateLockDataServerData(ServerData serverData, Server server) { - // 'true' if the bucket associated with this 'ServerData' - // doesn't belong to the remote server, as far as we can tell - boolean serverMismatch = - Bucket.bucketToServer(serverData.bucketNumber) != server; - - // each 'LockEntry' instance corresponds to the current holder - // of a lock, and all requestors waiting for it to be freed - for (LockEntry le : serverData.globalLocks.keyToEntry.values()) { - // update maximum 'key' and 'ownerKey' lengths - updateKeyLength(le.key); - updateOwnerKeyLength(le.currentOwnerKey); - - // fetch uuid - UUID uuid = le.currentOwnerUuid; - - // fetch/generate 'MergeData' instance for this UUID - MergedData md = mergedDataMap.computeIfAbsent(uuid, key -> new MergedData(uuid)); - - // update 'lockEntries' table entry - if (lockEntries.get(le.key) != null) { - md.comment("Duplicate server entry for key " + le.key); - } else { - lockEntries.put(le.key, le); - } - - // update 'MergedData.serverLockEntry' - // (leave 'MergedData.serverWaiting' as 'null', because - // this field is only used for waiting entries) - if (md.serverLockEntry == null) { - md.serverLockEntry = le; - } else { - md.comment("Duplicate server entry for UUID"); - } - - if (serverMismatch) { - // need to generate an additional error - md.comment(server.toString() - + "(server) does not own bucket " - + serverData.bucketNumber); - } - - // we need 'MergeData' entries for all waiting requests - for (Waiting waiting : le.waitingList) { - populateLockDataServerDataWaiting( - serverData, server, serverMismatch, le, waiting); - } - } - } - - private void populateLockDataServerDataWaiting( - ServerData serverData, Server server, boolean serverMismatch, - LockEntry le, Waiting waiting) { - - // update maximum 'ownerKey' length - updateOwnerKeyLength(waiting.ownerKey); - - // fetch uuid - UUID uuid = waiting.ownerUuid; - - // fetch/generate 'MergeData' instance for this UUID - MergedData md = mergedDataMap.computeIfAbsent(uuid, key -> new MergedData(uuid)); - - // update 'MergedData.serverLockEntry' and - // 'MergedData.serverWaiting' - if (md.serverLockEntry == null) { - md.serverLockEntry = le; - md.serverWaiting = waiting; - } else { - md.comment("Duplicate server entry for UUID"); - } - - if (serverMismatch) { - // need to generate an additional error - md.comment(server.toString() - + "(server) does not own bucket " - + serverData.bucketNumber); - } - } - - /** - * Do some additional sanity checks on the 'MergedData', and then - * display all of the results. - * - * @param out where the output should be displayed - */ - void dump(PrintStream out) { - // iterate over the 'MergedData' instances looking for problems - for (MergedData md : mergedDataMap.values()) { - if (md.clientDataRecord == null) { - md.comment("Client data missing"); - } else if (md.serverLockEntry == null) { - md.comment("Server data missing"); - clientOnlyEntries.put(md.clientDataRecord.identity.key, md); - } else if (!md.clientDataRecord.identity.key.equals(md.serverLockEntry.key)) { - md.comment("Client key(" + md.clientDataRecord.identity.key - + ") server key(" + md.serverLockEntry.key - + ") mismatch"); - } else { - String serverOwnerKey = (md.serverWaiting == null - ? md.serverLockEntry.currentOwnerKey : md.serverWaiting.ownerKey); - if (!md.clientDataRecord.identity.ownerKey.equals(serverOwnerKey)) { - md.comment("Client owner key(" - + md.clientDataRecord.identity.ownerKey - + ") server owner key(" + serverOwnerKey - + ") mismatch"); - } - // TBD: test for state mismatch - } - } - - dumpMergeData(out); - dumpServerTable(out); - dumpClientOnlyEntries(out); - } - - private void dumpMergeData(PrintStream out) { - if (detail) { - // generate format based upon the maximum key length, maximum - // owner key length, and whether comments are included anywhere - format = "%-" + keyLength + "s %6s %-9s %-" + ownerKeyLength - + "s %6s %-9s %-9s %-10s" + (commentsIncluded ? " %s\n" : "\n"); - - // dump out the header - out.printf(format, "Key", "Bucket", "Host UUID", - "Owner Key", "Bucket", "Host UUID", - "Lock UUID", "State", "Comments"); - out.printf(format, "---", "------", PRINTOUT_DASHES, - PRINTOUT_DASHES, "------", PRINTOUT_DASHES, - PRINTOUT_DASHES, "-----", "--------"); - } else { - // generate format based upon the maximum key length, maximum - // owner key length, and whether comments are included anywhere - format = "%-" + keyLength + "s %-" + ownerKeyLength - + "s %-36s %-10s" + (commentsIncluded ? " %s\n" : "\n"); - - // dump out the header - out.printf(format, "Key", "Owner Key", "UUID", "State", "Comments"); - out.printf(format, "---", PRINTOUT_DASHES, "----", "-----", "--------"); - } - } - - private void dumpServerTable(PrintStream out) { - // iterate over the server table - for (LockEntry le : lockEntries.values()) { - // fetch merged data - MergedData md = mergedDataMap.get(le.currentOwnerUuid); - - // dump out record associated with lock owner - if (detail) { - out.printf(format, - le.key, getBucket(le.key), bucketOwnerUuid(le.key), - le.currentOwnerKey, getBucket(le.currentOwnerKey), - bucketOwnerUuid(le.currentOwnerKey), - abbrevUuid(le.currentOwnerUuid), - md.getState(), md.firstComment()); - } else { - out.printf(format, - le.key, le.currentOwnerKey, le.currentOwnerUuid, - md.getState(), md.firstComment()); - } - dumpMoreComments(out, md); - - // iterate over all requests waiting for this lock - for (Waiting waiting: le.waitingList) { - // fetch merged data - md = mergedDataMap.get(waiting.ownerUuid); - - // dump out record associated with waiting request - if (detail) { - out.printf(format, - "", "", "", - waiting.ownerKey, getBucket(waiting.ownerKey), - bucketOwnerUuid(waiting.ownerKey), - abbrevUuid(waiting.ownerUuid), - md.getState(), md.firstComment()); - } else { - out.printf(format, "", waiting.ownerKey, waiting.ownerUuid, - md.getState(), md.firstComment()); - } - dumpMoreComments(out, md); - } - } - } - - private void dumpClientOnlyEntries(PrintStream out) { - // client records that don't have matching server entries - for (MergedData md : clientOnlyEntries.values()) { - ClientDataRecord cdr = md.clientDataRecord; - if (detail) { - out.printf(format, - cdr.identity.key, getBucket(cdr.identity.key), - bucketOwnerUuid(cdr.identity.key), - cdr.identity.ownerKey, - getBucket(cdr.identity.ownerKey), - bucketOwnerUuid(cdr.identity.ownerKey), - abbrevUuid(cdr.identity.uuid), - md.getState(), md.firstComment()); - } else { - out.printf(format, cdr.identity.key, cdr.identity.ownerKey, - cdr.identity.uuid, md.getState(), md.firstComment()); - } - dumpMoreComments(out, md); - } - } - - /** - * This method converts a String keyword into the corresponding bucket - * number. - * - * @param key the keyword to be converted - * @return the bucket number - */ - private static int getBucket(String key) { - return Bucket.bucketNumber(key); - } - - /** - * Determine the abbreviated UUID associated with a key. - * - * @param key the keyword to be converted - * @return the abbreviated UUID of the bucket owner - */ - private static String bucketOwnerUuid(String key) { - // fetch the bucket - Bucket bucket = Bucket.getBucket(Bucket.bucketNumber(key)); - - // fetch the bucket owner (may be 'null' if unassigned) - Server owner = bucket.getOwner(); - - return owner == null ? "NONE" : abbrevUuid(owner.getUuid()); - } - - /** - * Convert a UUID to an abbreviated form, which is the - * first 8 hex digits of the UUID, followed by the character '*'. - * - * @param uuid the UUID to convert - * @return the abbreviated form - */ - private static String abbrevUuid(UUID uuid) { - return uuid.toString().substring(0, 8) + "*"; - } - - /** - * If the 'MergedData' instance has more than one comment, - * dump out comments 2-n. - * - * @param out where the output should be displayed - * @param md the MergedData instance - */ - void dumpMoreComments(PrintStream out, MergedData md) { - if (md.comments.size() > 1) { - Queue<String> comments = new LinkedList<>(md.comments); - - // remove the first entry, because it has already been displayed - comments.remove(); - for (String comment : comments) { - if (detail) { - out.printf(format, "", "", "", "", "", "", "", "", comment); - } else { - out.printf(format, "", "", "", "", comment); - } - } - } - } - - /** - * Check the length of the specified 'key', and update 'keyLength' if - * it exceeds the current maximum. - * - * @param key the key to be tested - */ - void updateKeyLength(String key) { - int length = key.length(); - if (length > keyLength) { - keyLength = length; - } - } - - /** - * Check the length of the specified 'ownerKey', and update - * 'ownerKeyLength' if it exceeds the current maximum. - * - * @param ownerKey the owner key to be tested - */ - void updateOwnerKeyLength(String ownerKey) { - int length = ownerKey.length(); - if (length > ownerKeyLength) { - ownerKeyLength = length; - } - } - - /* ============================== */ - - /** - * Each instance of this class corresponds to client and/or server - * data structures, and is used to check consistency between the two. - */ - class MergedData { - // the client/server UUID - UUID uuid; - - // client-side data (from LocalLocks) - ClientDataRecord clientDataRecord = null; - - // server-side data (from GlobalLocks) - LockEntry serverLockEntry = null; - Waiting serverWaiting = null; - - // detected problems, such as server/client mismatches - Queue<String> comments = new LinkedList<>(); - - /** - * Constructor - initialize the 'uuid'. - * - * @param uuid the UUID that identifies the original 'TargetLock' - */ - MergedData(UUID uuid) { - this.uuid = uuid; - } - - /** - * add a comment to the list, and indicate that there are now - * comments present. - * - * @param co the comment to add - */ - void comment(String co) { - comments.add(co); - commentsIncluded = true; - } - - /** - * Return the first comment, or an empty string if there are no - * comments. - * - * @return the first comment, or an empty string if there are no - * comments (useful for formatting output). - */ - String firstComment() { - return comments.isEmpty() ? "" : comments.poll(); - } - - /** - * Return a string description of the state. - * - * @return a string description of the state. - */ - String getState() { - return clientDataRecord == null - ? "MISSING" : clientDataRecord.state.toString(); - } - } - - /** - * This class contains all of the data sent from each host to the - * host that is consolidating the information for display. - */ - static class HostData implements Serializable { - private static final long serialVersionUID = 1L; - - // the UUID of the host sending the data - private UUID hostUuid; - - // all of the information derived from the 'LocalLocks' data - private List<ClientData> clientDataList; - - // all of the information derived from the 'GlobalLocks' data - private List<ServerData> serverDataList; - - /** - * Constructor - this goes through all of the lock tables, - * and populates 'clientDataList' and 'serverDataList'. - */ - HostData() { - // fetch UUID - hostUuid = Server.getThisServer().getUuid(); - - // initial storage for client and server data - clientDataList = new ArrayList<>(); - serverDataList = new ArrayList<>(); - - // go through buckets - for (int i = 0; i < Bucket.BUCKETCOUNT; i += 1) { - Bucket bucket = Bucket.getBucket(i); - - // client data - LocalLocks localLocks = - bucket.getAdjunctDontCreate(LocalLocks.class); - if (localLocks != null) { - // we have client data for this bucket - ClientData clientData = new ClientData(i); - clientDataList.add(clientData); - - synchronized (localLocks) { - generateClientLockData(localLocks, clientData); - } - } - - // server data - GlobalLocks globalLocks = - bucket.getAdjunctDontCreate(GlobalLocks.class); - if (globalLocks != null) { - // server data is already in serializable form - serverDataList.add(new ServerData(i, globalLocks)); - } - } - } - - private void generateClientLockData(LocalLocks localLocks, ClientData clientData) { - for (WeakReference<TargetLock> wr : - localLocks.weakReferenceToIdentity.keySet()) { - // Note: 'targetLock' may be 'null' if it has - // been abandoned, and garbage collected - TargetLock targetLock = wr.get(); - - // fetch associated 'identity' - Identity identity = - localLocks.weakReferenceToIdentity.get(wr); - if (identity != null) { - // add a new 'ClientDataRecord' for this bucket - clientData.clientDataRecords.add( - new ClientDataRecord(identity, - (targetLock == null ? null : - targetLock.getState()))); - } - } - } - } - - /** - * Information derived from the 'LocalLocks' adjunct to a single bucket. - */ - static class ClientData implements Serializable { - private static final long serialVersionUID = 1L; - - // number of the bucket - private int bucketNumber; - - // all of the client locks within this bucket - private List<ClientDataRecord> clientDataRecords; - - /** - * Constructor - initially, there are no 'clientDataRecords'. - * - * @param bucketNumber the bucket these records are associated with - */ - ClientData(int bucketNumber) { - this.bucketNumber = bucketNumber; - clientDataRecords = new ArrayList<>(); - } - } - - /** - * This corresponds to the information contained within a - * single 'TargetLock'. - */ - static class ClientDataRecord implements Serializable { - private static final long serialVersionUID = 1L; - - // contains key, ownerKey, uuid - private Identity identity; - - // state field of 'TargetLock' - // (may be 'null' if there is no 'TargetLock') - private State state; - - /** - * Constructor - initialize the fields. - * - * @param identity contains key, ownerKey, uuid - * @param state the state if the 'TargetLock' exists, and 'null' if it - * has been garbage collected - */ - ClientDataRecord(Identity identity, State state) { - this.identity = identity; - this.state = state; - } - } - - /** - * Information derived from the 'GlobalLocks' adjunct to a single bucket. - */ - static class ServerData implements Serializable { - private static final long serialVersionUID = 1L; - - // number of the bucket - private int bucketNumber; - - // server-side data associated with a single bucket - private GlobalLocks globalLocks; - - /** - * Constructor - initialize the fields. - * - * @param bucketNumber the bucket these records are associated with - * @param globalLocks GlobalLocks instance associated with 'bucketNumber' - */ - ServerData(int bucketNumber, GlobalLocks globalLocks) { - this.bucketNumber = bucketNumber; - this.globalLocks = globalLocks; - } - } - } - - /* ============================================================ */ - - /** - * Instances of 'AuditData' are passed between servers as part of the - * 'TargetLock' audit. - */ - static class AuditData implements Serializable { - private static final long serialVersionUID = 1L; - - // client records that currently exist, or records to be cleared - // (depending upon message) -- client/server is from the senders side - private List<Identity> clientData; - - // server records that currently exist, or records to be cleared - // (depending upon message) -- client/server is from the senders side - private List<Identity> serverData; - - /** - * Constructor - set 'hostUuid' to the current host, and start with - * empty lists. - */ - AuditData() { - clientData = new ArrayList<>(); - serverData = new ArrayList<>(); - } - - /** - * This is called when we receive an incoming 'AuditData' object from - * a remote host. - * - * @param includeWarnings if 'true', generate warning messages - * for mismatches - * @return an 'AuditData' instance that only contains records we - * can't confirm - */ - AuditData generateResponse(boolean includeWarnings) { - AuditData response = new AuditData(); - - // compare remote servers client data with our server data - generateResponseClientEnd(response, includeWarnings); - - // test server data - generateResponseServerEnd(response, includeWarnings); - - return response; - } - - private void generateResponseClientEnd(AuditData response, boolean includeWarnings) { - for (Identity identity : clientData) { - // remote end is the client, and we are the server - Bucket bucket = Bucket.getBucket(identity.key); - GlobalLocks globalLocks = - bucket.getAdjunctDontCreate(GlobalLocks.class); - - if (globalLocks != null) { - Map<String, LockEntry> keyToEntry = globalLocks.keyToEntry; - synchronized (keyToEntry) { - if (matchIdentity(keyToEntry, identity)) { - continue; - } - } - } - - // If we reach this point, a match was not confirmed. Note that it - // is possible that we got caught in a transient state, so we need - // to somehow make sure that we don't "correct" a problem that - // isn't real. - - if (includeWarnings) { - logger.warn("TargetLock audit issue: server match not found " - + "(key={},ownerKey={},uuid={})", - identity.key, identity.ownerKey, identity.uuid); - } - - // it was 'clientData' to the sender, but 'serverData' to us - response.serverData.add(identity); - } - } - - private boolean matchIdentity(Map<String, LockEntry> keyToEntry, Identity identity) { - LockEntry le = keyToEntry.get(identity.key); - if (le != null) { - if (identity.uuid.equals(le.currentOwnerUuid) - && identity.ownerKey.equals(le.currentOwnerKey)) { - // we found a match - return true; - } - - // check the waiting list - for (Waiting waiting : le.waitingList) { - if (identity.uuid.equals(waiting.ownerUuid) - && identity.ownerKey.equals(waiting.ownerKey)) { - // we found a match on the waiting list - return true; - } - } - } - - return false; - } - - private void generateResponseServerEnd(AuditData response, boolean includeWarnings) { - for (Identity identity : serverData) { - // remote end is the server, and we are the client - Bucket bucket = Bucket.getBucket(identity.ownerKey); - LocalLocks localLocks = - bucket.getAdjunctDontCreate(LocalLocks.class); - if (localLocks != null) { - synchronized (localLocks) { - WeakReference<TargetLock> wr = - localLocks.uuidToWeakReference.get(identity.uuid); - if (wr != null) { - Identity identity2 = - localLocks.weakReferenceToIdentity.get(wr); - if (identity2 != null - && identity.key.equals(identity2.key) - && identity.ownerKey.equals(identity2.ownerKey)) { - // we have a match - continue; - } - } - } - } - - // If we reach this point, a match was not confirmed. Note that it - // is possible that we got caught in a transient state, so we need - // to somehow make sure that we don't "correct" a problem that - // isn't real. - if (includeWarnings) { - logger.warn("TargetLock audit issue: client match not found " - + "(key={},ownerKey={},uuid={})", - identity.key, identity.ownerKey, identity.uuid); - } - response.clientData.add(identity); - } - } - - /** - * The response messages contain 'Identity' objects that match those - * in our outgoing '/lock/audit' message, but that the remote end could - * not confirm. Again, the definition of 'client' and 'server' are - * the remote ends' version. - * - * @param server the server we sent the request to - */ - void processResponse(Server server) { - if (clientData.isEmpty() && serverData.isEmpty()) { - // no mismatches - logger.info("TargetLock audit with {} completed -- no mismatches", - server); - return; - } - - // There are still mismatches -- note that 'clientData' and - // 'serverData' are from the remote end's perspective, which is the - // opposite of this end - - for (Identity identity : clientData) { - // these are on our server end -- we were showing a lock on this - // end, but the other end has no such client - logger.error("Audit mismatch (GlobalLocks): (key={},owner={},uuid={})", - identity.key, identity.ownerKey, identity.uuid); - - // free the lock - GlobalLocks.get(identity.key).unlock(identity.key, identity.uuid); - } - for (Identity identity : serverData) { - // these are on our client end - logger.error("Audit mismatch (LocalLocks): (key={},owner={},uuid={})", - identity.key, identity.ownerKey, identity.uuid); - - // clean up 'LocalLocks' tables - LocalLocks localLocks = LocalLocks.get(identity.ownerKey); - TargetLock targetLock = null; - synchronized (localLocks) { - WeakReference<TargetLock> wr = - localLocks.uuidToWeakReference.get(identity.uuid); - if (wr != null) { - targetLock = wr.get(); - localLocks.weakReferenceToIdentity.remove(wr); - localLocks.uuidToWeakReference - .remove(identity.uuid); - wr.clear(); - } - } - - if (targetLock != null) { - // may need to update state - synchronized (targetLock) { - if (targetLock.state != State.FREE) { - targetLock.state = State.LOST; - } - } - } - } - logger.info("TargetLock audit with {} completed -- {} mismatches", - server, clientData.size() + serverData.size()); - } - - /** - * Serialize and base64-encode this 'AuditData' instance, so it can - * be sent in a message. - * - * @return a byte array, which can be decoded and deserialized at - * the other end ('null' is returned if there were any problems) - */ - byte[] encode() { - try { - return Base64.getEncoder().encode(Util.serialize(this)); - } catch (IOException e) { - logger.error("TargetLock.AuditData.encode Exception", e); - return EMPTY_BYTE_ARRAY; - } - } - - /** - * Base64-decode and deserialize a byte array. - * - * @param encodedData a byte array encoded via 'AuditData.encode' - * (typically on the remote end of a connection) - * @return an 'AuditData' instance if decoding was successful, - * and 'null' if not - */ - static AuditData decode(byte[] encodedData) { - try { - Object decodedData = - Util.deserialize(Base64.getDecoder().decode(encodedData)); - if (decodedData instanceof AuditData) { - return (AuditData) decodedData; - } else { - logger.error( - "TargetLock.AuditData.decode returned instance of class {}", - decodedData.getClass().getName()); - return null; - } - } catch (IOException | ClassNotFoundException e) { - logger.error("TargetLock.AuditData.decode Exception", e); - return null; - } - } - } - - /** - * This class contains methods that control the audit. Also, sn instance of - * 'Audit' is created for each audit that is in progress. - */ - static class Audit { - // if non-null, it means that we have a timer set that periodicall - // triggers the audit - static TimerTask timerTask = null; - - // maps 'Server' to audit data associated with that server - Map<Server, AuditData> auditMap = new IdentityHashMap<>(); - - /** - * Run a single audit cycle. - */ - static void runAudit() { - logger.info("Starting TargetLock audit"); - Audit audit = new Audit(); - - // populate 'auditMap' table - audit.build(); - - // send to all of the servers in 'auditMap' (may include this server) - audit.send(); - } - - /** - * Schedule the audit to run periodically based upon defined properties. - */ - static void scheduleAudit() { - scheduleAudit(auditPeriod, auditGracePeriod); - } - - /** - * Schedule the audit to run periodically -- all of the hosts arrange to - * run their audit at a different time, evenly spaced across the audit - * period. - * - * @param period how frequently to run the audit, in milliseconds - * @param gracePeriod ensure that the audit doesn't run until at least - * 'gracePeriod' milliseconds have passed from the current time - */ - static synchronized void scheduleAudit(final long period, final long gracePeriod) { - - if (timerTask != null) { - // cancel current timer - timerTask.cancel(); - timerTask = null; - } - - // this needs to run in the 'MainLoop' thread, because it is dependent - // upon the list of servers, and our position in this list - MainLoop.queueWork(() -> { - // this runs in the 'MainLoop' thread - - // current list of servers - Collection<Server> servers = Server.getServers(); - - // count of the number of servers - int count = servers.size(); - - // will contain our position in this list - int index = 0; - - // current server - Server thisServer = Server.getThisServer(); - - for (Server server : servers) { - if (server == thisServer) { - break; - } - index += 1; - } - - // if index == count, we didn't find this server - // (which shouldn't happen) - - if (index < count) { - // The servers are ordered by UUID, and 'index' is this - // server's position within the list. Suppose the period is - // 60000 (60 seconds), and there are 5 servers -- the first one - // will run the audit at 0 seconds after the minute, the next - // at 12 seconds after the minute, then 24, 36, 48. - long offset = (period * index) / count; - - // the earliest time we want the audit to run - long time = System.currentTimeMillis() + gracePeriod; - long startTime = time - (time % period) + offset; - if (startTime <= time) { - startTime += period; - } - synchronized (Audit.class) { - if (timerTask != null) { - timerTask.cancel(); - } - timerTask = new TimerTask() { - @Override - public void run() { - runAudit(); - } - }; - - // now, schedule the timer - Util.timer.scheduleAtFixedRate( - timerTask, new Date(startTime), period); - } - } - }); - } - - /** - * Handle an incoming '/lock/audit' message. - * - * @param serverUuid the UUID of the intended destination server - * @param ttl similar to IP time-to-live -- it controls the number of hops - * @param data base64-encoded data, containing a serialized 'AuditData' - * instance - * @return a serialized and base64-encoded 'AuditData' response - */ - static byte[] incomingAudit(UUID serverUuid, int ttl, byte[] encodedData) { - if (!Server.getThisServer().getUuid().equals(serverUuid)) { - ttl -= 1; - if (ttl > 0) { - Server server = Server.getServer(serverUuid); - if (server != null) { - WebTarget webTarget = server.getWebTarget(LOCK_AUDIT); - if (webTarget != null) { - logger.info("Forwarding {} to uuid {}", LOCK_AUDIT, - serverUuid); - Entity<String> entity = - Entity.entity(new String(encodedData), - MediaType.APPLICATION_OCTET_STREAM_TYPE); - return webTarget - .queryParam(QP_SERVER, serverUuid.toString()) - .queryParam(QP_TTL, String.valueOf(ttl)) - .request().post(entity, byte[].class); - } - } - } - - // if we reach this point, we didn't forward for some reason - - logger.error("Couldn't forward {} to uuid {}", LOCK_AUDIT, - serverUuid); - return EMPTY_BYTE_ARRAY; - } - - AuditData auditData = AuditData.decode(encodedData); - if (auditData != null) { - AuditData auditResp = auditData.generateResponse(true); - return auditResp.encode(); - } - return EMPTY_BYTE_ARRAY; - } - - /** - * This method populates the 'auditMap' table by going through all of - * the client and server lock data, and sorting it according to the - * remote server. - */ - void build() { - for (int i = 0; i < Bucket.BUCKETCOUNT; i += 1) { - Bucket bucket = Bucket.getBucket(i); - - // client data - buildClientData(bucket); - - // server data - buildServerData(bucket); - } - } - - private void buildClientData(Bucket bucket) { - // client data - LocalLocks localLocks = - bucket.getAdjunctDontCreate(LocalLocks.class); - if (localLocks != null) { - synchronized (localLocks) { - // we have client data for this bucket - for (Identity identity : - localLocks.weakReferenceToIdentity.values()) { - // find or create the 'AuditData' instance associated - // with the server owning the 'key' - AuditData auditData = getAuditData(identity.key); - if (auditData != null) { - auditData.clientData.add(identity); - } - } - } - } - } - - private void buildServerData(Bucket bucket) { - // server data - GlobalLocks globalLocks = - bucket.getAdjunctDontCreate(GlobalLocks.class); - if (globalLocks != null) { - // we have server data for this bucket - Map<String, LockEntry> keyToEntry = globalLocks.keyToEntry; - synchronized (keyToEntry) { - for (LockEntry le : keyToEntry.values()) { - // find or create the 'AuditData' instance associated - // with the current 'ownerKey' - AuditData auditData = getAuditData(le.currentOwnerKey); - if (auditData != null) { - // create an 'Identity' entry, and add it to - // the list associated with the remote server - auditData.serverData.add( - new Identity(le.key, le.currentOwnerKey, - le.currentOwnerUuid)); - } - - for (Waiting waiting : le.waitingList) { - // find or create the 'AuditData' instance associated - // with the waiting entry 'ownerKey' - auditData = getAuditData(waiting.ownerKey); - if (auditData != null) { - // create an 'Identity' entry, and add it to - // the list associated with the remote server - auditData.serverData.add( - new Identity(le.key, waiting.ownerKey, - waiting.ownerUuid)); - } - } - } - } - } - } - - /** - * Find or create the 'AuditData' structure associated with a particular - * key. - */ - AuditData getAuditData(String key) { - // map 'key -> bucket number', and then 'bucket number' -> 'server' - Server server = Bucket.bucketToServer(Bucket.bucketNumber(key)); - if (server != null) { - return auditMap.computeIfAbsent(server, sk -> new AuditData()); - } - - // this happens when the bucket has not been assigned to a server yet - return null; - } - - /** - * Using the collected 'auditMap', send out the messages to all of the - * servers. - */ - void send() { - if (auditMap.isEmpty()) { - logger.info("TargetLock audit: no locks on this server"); - } else { - logger.info("TargetLock audit: sending audit information to {}", - auditMap.keySet()); - } - - for (final Server server : auditMap.keySet()) { - sendServer(server); - } - } - - private void sendServer(final Server server) { - // fetch audit data - AuditData auditData = auditMap.get(server); - - if (server == Server.getThisServer()) { - // process this locally - final AuditData respData = auditData.generateResponse(true); - if (respData.clientData.isEmpty() - && respData.serverData.isEmpty()) { - // no mismatches - logger.info("{} no errors from self ({})", TARGETLOCK_AUDIT_SEND, server); - return; - } - - // do the rest in a separate thread - server.getThreadPool().execute(() -> { - // wait a few seconds, and see if we still know of these - // errors - if (AuditPostResponse.responseSupport( - respData, "self (" + server + ")", - "TargetLock.Audit.send")) { - // a return value of 'true' either indicates the - // mismatches were resolved after a retry, or we - // received an interrupt, and need to abort - return; - } - - // any mismatches left in 'respData' are still issues - respData.processResponse(server); - }); - return; - } - - // serialize - byte[] encodedData = auditData.encode(); - if (encodedData.length == 0) { - // error has already been displayed - return; - } - - // generate entity - Entity<String> entity = - Entity.entity(new String(encodedData), - MediaType.APPLICATION_OCTET_STREAM_TYPE); - - server.post(LOCK_AUDIT, entity, new AuditPostResponse(server)); - } - } - - static class AuditPostResponse implements Server.PostResponse { - private Server server; - - AuditPostResponse(Server server) { - this.server = server; - } - - @Override - public WebTarget webTarget(WebTarget webTarget) { - // include the 'uuid' keyword - return webTarget - .queryParam(QP_SERVER, server.getUuid().toString()) - .queryParam(QP_TTL, timeToLive); - } - - @Override - public void response(Response response) { - // process the response here - AuditData respData = - AuditData.decode(response.readEntity(byte[].class)); - if (respData == null) { - logger.error("{} couldn't process response from {}", - TARGETLOCK_AUDIT_SEND, server); - return; - } - - // if we reach this point, we got a response - if (respData.clientData.isEmpty() - && respData.serverData.isEmpty()) { - // no mismatches - logger.info("{} no errors from {}", TARGETLOCK_AUDIT_SEND, server); - return; - } - - // wait a few seconds, and see if we still know of these - // errors - if (responseSupport(respData, server, "AuditPostResponse.response")) { - // a return falue of 'true' either indicates the mismatches - // were resolved after a retry, or we received an interrupt, - // and need to abort - return; - } - - // any mismatches left in 'respData' are still there -- - // hopefully, they are transient issues on the other side - AuditData auditData = new AuditData(); - auditData.clientData = respData.serverData; - auditData.serverData = respData.clientData; - - // serialize - byte[] encodedData = auditData.encode(); - if (encodedData.length == 0) { - // error has already been displayed - return; - } - - // generate entity - Entity<String> entity = - Entity.entity(new String(encodedData), - MediaType.APPLICATION_OCTET_STREAM_TYPE); - - // send new list to other end - response = server - .getWebTarget(LOCK_AUDIT) - .queryParam(QP_SERVER, server.getUuid().toString()) - .queryParam(QP_TTL, timeToLive) - .request().post(entity); - - respData = AuditData.decode(response.readEntity(byte[].class)); - if (respData == null) { - logger.error("TargetLock.auditDataBuilder.send: " - + "couldn't process response from {}", - server); - return; - } - - // if there are mismatches left, they are presumably real - respData.processResponse(server); - } - - // Handle mismatches indicated by an audit response -- a return value of - // 'true' indicates that there were no mismatches after a retry, or - // we received an interrupt. In either case, the caller returns. - private static boolean responseSupport(AuditData respData, Object serverString, String caller) { - logger.info("{}: mismatches from {}", caller, serverString); - try { - Thread.sleep(auditRetryDelay); - } catch (InterruptedException e) { - logger.error("{}: Interrupted handling audit response from {}", - caller, serverString); - // just abort - Thread.currentThread().interrupt(); - return true; - } - - // This will check against our own data -- any mismatches - // mean that things have changed since we sent out the - // first message. We will remove any mismatches from - // 'respData', and see if there are any left. - AuditData mismatches = respData.generateResponse(false); - - respData.serverData.removeAll(mismatches.clientData); - respData.clientData.removeAll(mismatches.serverData); - - if (respData.clientData.isEmpty() - && respData.serverData.isEmpty()) { - // no mismatches -- - // there must have been transient issues on our side - logger.info("{}: no mismatches from {} after retry", - caller, serverString); - return true; - } - - return false; - } - } -} diff --git a/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/Util.java b/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/Util.java deleted file mode 100644 index 92d0994c..00000000 --- a/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/Util.java +++ /dev/null @@ -1,183 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * feature-server-pool - * ================================================================================ - * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved. - * ================================================================================ - * 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. - * ============LICENSE_END========================================================= - */ - -package org.onap.policy.drools.serverpool; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.nio.charset.StandardCharsets; -import java.util.Comparator; -import java.util.Timer; -import java.util.UUID; -import org.apache.commons.io.IOUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class Util { - private static Logger logger = LoggerFactory.getLogger(Util.class); - // create a shared 'Timer' instance - public static final Timer timer = new Timer("Server Pool Timer", true); - - /** - * Hide implicit public constructor. - */ - private Util() { - // everything here is static -- no instances of this class are created - } - - /** - * Internally, UUID objects use two 'long' variables, and the default - * comparison is signed, which means the order for the first and 16th digit - * is: '89abcdef01234567', while the order for the rest is - * '0123456789abcdef'. - * The following comparator uses the ordering '0123456789abcdef' for all - * digits. - */ - public static final Comparator<UUID> uuidComparator = (UUID u1, UUID u2) -> { - // compare most significant portion - int rval = Long.compareUnsigned(u1.getMostSignificantBits(), - u2.getMostSignificantBits()); - if (rval == 0) { - // most significant portion matches -- - // compare least significant portion - rval = Long.compareUnsigned(u1.getLeastSignificantBits(), - u2.getLeastSignificantBits()); - } - return rval; - }; - - /* ============================================================ */ - - /** - * write a UUID to an output stream. - * - * @param ds the output stream - * @param uuid the uuid to write - */ - public static void writeUuid(DataOutputStream ds, UUID uuid) throws IOException { - // write out 16 byte UUID - ds.writeLong(uuid.getMostSignificantBits()); - ds.writeLong(uuid.getLeastSignificantBits()); - } - - /** - * read a UUID from an input stream. - * - * @param ds the input stream - */ - public static UUID readUuid(DataInputStream ds) throws IOException { - long mostSigBits = ds.readLong(); - long leastSigBits = ds.readLong(); - return new UUID(mostSigBits, leastSigBits); - } - - /* ============================================================ */ - - /** - * Read from an 'InputStream' until EOF or until it is closed. This method - * may block, depending on the type of 'InputStream'. - * - * @param input This is the input stream - * @return A 'String' containing the contents of the input stream - */ - public static String inputStreamToString(InputStream input) { - try { - return IOUtils.toString(input, StandardCharsets.UTF_8); - } catch (IOException e) { - logger.error("Util.inputStreamToString error", e); - return ""; - } - } - - /* ============================================================ */ - - /** - * Serialize an object into a byte array. - * - * @param object the object to serialize - * @return a byte array containing the serialized object - * @throws IOException this may be an exception thrown by the output stream, - * a NotSerializableException if an object can't be serialized, or an - * InvalidClassException - */ - public static byte[] serialize(Object object) throws IOException { - try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); - ObjectOutputStream oos = new ObjectOutputStream(bos)) { - oos.writeObject(object); - oos.flush(); - return bos.toByteArray(); - } - } - - /** - * Deserialize a byte array into an object. - * - * @param data a byte array containing the serialized object - * @return the deserialized object - * @throws IOException this may be an exception thrown by the input stream, - * a StreamCorrupted Exception if the information in the stream is not - * consistent, an OptionalDataException if the input data primitive data, - * rather than an object, or InvalidClassException - * @throws ClassNotFoundException if the class of a serialized object can't - * be found - */ - public static Object deserialize(byte[] data) throws IOException, ClassNotFoundException { - try (ByteArrayInputStream bis = new ByteArrayInputStream(data); - ObjectInputStream ois = new ObjectInputStream(bis)) { - return ois.readObject(); - } - } - - /** - * Deserialize a byte array into an object. - * - * @param data a byte array containing the serialized object - * @param classLoader the class loader to use when locating classes - * @return the deserialized object - * @throws IOException this may be an exception thrown by the input stream, - * a StreamCorrupted Exception if the information in the stream is not - * consistent, an OptionalDataException if the input data primitive data, - * rather than an object, or InvalidClassException - * @throws ClassNotFoundException if the class of a serialized object can't - * be found - */ - public static Object deserialize(byte[] data, ClassLoader classLoader) - throws IOException, ClassNotFoundException { - - try (ByteArrayInputStream bis = new ByteArrayInputStream(data); - ExtendedObjectInputStream ois = - new ExtendedObjectInputStream(bis, classLoader)) { - return ois.readObject(); - } - } - - /** - * Shutdown the timer thread. - */ - public static void shutdown() { - timer.cancel(); - } -} diff --git a/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/persistence/Persistence.java b/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/persistence/Persistence.java deleted file mode 100644 index 18afcd47..00000000 --- a/feature-server-pool/src/main/java/org/onap/policy/drools/serverpool/persistence/Persistence.java +++ /dev/null @@ -1,899 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * feature-server-pool - * ================================================================================ - * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved. - * ================================================================================ - * 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. - * ============LICENSE_END========================================================= - */ - -package org.onap.policy.drools.serverpool.persistence; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.ObjectOutputStream; -import java.util.Base64; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.IdentityHashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Properties; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import javax.ws.rs.Consumes; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.QueryParam; -import javax.ws.rs.client.Entity; -import javax.ws.rs.client.WebTarget; -import javax.ws.rs.core.MediaType; -import org.kie.api.event.rule.ObjectDeletedEvent; -import org.kie.api.event.rule.ObjectInsertedEvent; -import org.kie.api.event.rule.ObjectUpdatedEvent; -import org.kie.api.event.rule.RuleRuntimeEventListener; -import org.kie.api.runtime.KieSession; -import org.onap.policy.drools.core.DroolsRunnable; -import org.onap.policy.drools.core.PolicyContainer; -import org.onap.policy.drools.core.PolicySession; -import org.onap.policy.drools.core.PolicySessionFeatureApi; -import org.onap.policy.drools.serverpool.Bucket; -import org.onap.policy.drools.serverpool.Keyword; -import org.onap.policy.drools.serverpool.Server; -import org.onap.policy.drools.serverpool.ServerPoolApi; -import org.onap.policy.drools.serverpool.TargetLock.GlobalLocks; -import org.onap.policy.drools.serverpool.Util; -import org.onap.policy.drools.system.PolicyControllerConstants; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * This class provides a persistence implementation for 'feature-server-pool', - * backing up the data of selected Drools sessions and server-side 'TargetLock' - * data on separate hosts. - */ -public class Persistence extends ServerPoolApi implements PolicySessionFeatureApi { - private static Logger logger = LoggerFactory.getLogger(Persistence.class); - - // HTTP query parameters - private static final String QP_BUCKET = "bucket"; - private static final String QP_SESSION = "session"; - private static final String QP_COUNT = "count"; - private static final String QP_DEST = "dest"; - - /* ************************************* */ - /* 'PolicySessionFeatureApi' interface */ - /* ************************************* */ - - /** - * {@inheritDoc} - */ - @Override - public int getSequenceNumber() { - return 1; - } - - /** - * {@inheritDoc} - */ - @Override - public void newPolicySession(PolicySession policySession) { - // a new Drools session is being created -- look at the properties - // 'persistence.<session-name>.type' and 'persistence.type' to determine - // whether persistence is enabled for this session - - // fetch properties file - PolicyContainer container = policySession.getPolicyContainer(); - Properties properties = PolicyControllerConstants.getFactory().get( - container.getGroupId(), container.getArtifactId()).getProperties(); - - // look at 'persistence.<session-name>.type', and 'persistence.type' - String type = properties.getProperty("persistence." + policySession.getName() + ".type"); - if (type == null) { - type = properties.getProperty("persistence.type"); - } - - if ("auto".equals(type) || "native".equals(type)) { - // persistence is enabled this session - policySession.setAdjunct(PersistenceRunnable.class, - new PersistenceRunnable(policySession)); - } - } - - /* *************************** */ - /* 'ServerPoolApi' interface */ - /* *************************** */ - - /** - * {@inheritDoc} - */ - @Override - public Collection<Class<?>> servletClasses() { - // the nested class 'Rest' contains additional REST calls - List<Class<?>> classes = new LinkedList<>(); - classes.add(Rest.class); - return classes; - } - - /** - * {@inheritDoc} - */ - @Override - public void restoreBucket(Bucket bucket) { - // if we reach this point, no data was received from the old server, which - // means we just initialized, or we did not have a clean bucket migration - - ReceiverBucketData rbd = bucket.removeAdjunct(ReceiverBucketData.class); - if (rbd != null) { - // there is backup data -- do a restore - rbd.restoreBucket(bucket); - } - } - - /** - * {@inheritDoc} - */ - @Override - public void lockUpdate(Bucket bucket, GlobalLocks globalLocks) { - // we received a notification from 'TargetLock' that 'GlobalLocks' data - // has changed (TBD: should any attempt be made to group updates that - // occur in close succession?) - - sendLockDataToBackups(bucket, globalLocks); - } - - /** - * {@inheritDoc} - */ - @Override - public void auditBucket(Bucket bucket, boolean isOwner, boolean isBackup) { - if (isOwner) { - // it may be that backup hosts have changed -- - // send out lock and session data - - // starting with lock data - GlobalLocks globalLocks = - bucket.getAdjunctDontCreate(GlobalLocks.class); - if (globalLocks != null) { - sendLockDataToBackups(bucket, globalLocks); - } - - // now, session data - SenderBucketData sbd = - bucket.getAdjunctDontCreate(SenderBucketData.class); - if (sbd != null) { - synchronized (sbd) { - // go through all of the sessions where we have persistent data - for (PolicySession session : sbd.sessionData.keySet()) { - Object obj = session.getAdjunct(PersistenceRunnable.class); - if (obj instanceof PersistenceRunnable) { - PersistenceRunnable pr = (PersistenceRunnable) obj; - synchronized (pr.modifiedBuckets) { - // mark bucket associated with this session - // as modified - pr.modifiedBuckets.add(bucket); - } - } - } - } - } - } else if (bucket.removeAdjunct(SenderBucketData.class) != null) { - logger.warn("Bucket {}: Removed superfluous " - + "'SenderBucketData' adjunct", - bucket.getIndex()); - } - if (!isBackup && bucket.removeAdjunct(ReceiverBucketData.class) != null) { - logger.warn("Bucket {}: Removed superfluous " - + "'ReceiverBucketData' adjunct", - bucket.getIndex()); - } - } - - /** - * This method supports 'lockUpdate' -- it has been moved to a separate - * 'static' method, so it can also be called after restoring 'GlobalLocks', - * so it can be backed up on its new servers. - * - * @param bucket the bucket containing the 'GlobalLocks' adjunct - * @param globalLocks the 'GlobalLocks' adjunct - */ - private static void sendLockDataToBackups(final Bucket bucket, final GlobalLocks globalLocks) { - final int bucketNumber = bucket.getIndex(); - SenderBucketData sbd = bucket.getAdjunct(SenderBucketData.class); - int lockCount = 0; - - // serialize the 'globalLocks' instance - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - try { - ObjectOutputStream oos = new ObjectOutputStream(bos); - synchronized (globalLocks) { - // the 'GlobalLocks' instance and counter are tied together - oos.writeObject(globalLocks); - lockCount = sbd.getLockCountAndIncrement(); - } - oos.close(); - } catch (IOException e) { - logger.error("Persistence.LockUpdate({})", bucketNumber, e); - return; - } - - // convert to Base64, and generate an 'Entity' for the REST call - byte[] serializedData = Base64.getEncoder().encode(bos.toByteArray()); - final Entity<String> entity = - Entity.entity(new String(serializedData), - MediaType.APPLICATION_OCTET_STREAM_TYPE); - final int count = lockCount; - - sendLocksToBackupServers(bucketNumber, entity, count, bucket.getBackups()); - } - - private static void sendLocksToBackupServers(final int bucketNumber, final Entity<String> entity, final int count, - Set<Server> servers) { - for (final Server server : servers) { - if (server != null) { - // send out REST command - server.getThreadPool().execute(() -> { - WebTarget webTarget = - server.getWebTarget("persistence/lock"); - if (webTarget != null) { - webTarget - .queryParam(QP_BUCKET, bucketNumber) - .queryParam(QP_COUNT, count) - .queryParam(QP_DEST, server.getUuid()) - .request().post(entity); - } - }); - } - } - } - - /* ============================================================ */ - - /** - * One instance of this class exists for every Drools session that is - * being backed up. It implements the 'RuleRuntimeEventListener' interface, - * so it receives notifications of Drools object changes, and also implements - * the 'DroolsRunnable' interface, so it can run within the Drools session - * thread, which should reduce the chance of catching an object in a - * transient state. - */ - static class PersistenceRunnable implements DroolsRunnable, - RuleRuntimeEventListener { - // this is the Drools session associated with this instance - private PolicySession session; - - // this is the string "<groupId>:<artifactId>:<sessionName>" - private String encodedSessionName; - - // the buckets in this session which have modifications that still - // need to be backed up - private Set<Bucket> modifiedBuckets = new HashSet<>(); - - /** - * Constructor - save the session information, and start listing for - * updates. - */ - PersistenceRunnable(PolicySession session) { - PolicyContainer pc = session.getPolicyContainer(); - - this.session = session; - this.encodedSessionName = - pc.getGroupId() + ":" + pc.getArtifactId() + ":" + session.getName(); - session.getKieSession().addEventListener(this); - } - - /* **************************** */ - /* 'DroolsRunnable' interface */ - /* **************************** */ - - /** - * {@inheritDoc} - */ - @Override - public void run() { - try { - // save a snapshot of 'modifiedBuckets' - Set<Bucket> saveModifiedBuckets; - synchronized (modifiedBuckets) { - saveModifiedBuckets = new HashSet<>(modifiedBuckets); - modifiedBuckets.clear(); - } - - // iterate over all of the modified buckets, sending update data - // to all of the backup servers - for (Bucket bucket : saveModifiedBuckets) { - SenderBucketData sbd = - bucket.getAdjunctDontCreate(SenderBucketData.class); - if (sbd != null) { - // serialization occurs within the Drools session thread - SenderSessionBucketData ssbd = sbd.getSessionData(session); - byte[] serializedData = - ssbd.getLatestEncodedSerializedData(); - final int count = ssbd.getCount(); - final Entity<String> entity = - Entity.entity(new String(serializedData), - MediaType.APPLICATION_OCTET_STREAM_TYPE); - - // build list of backup servers - Set<Server> servers = new HashSet<>(); - synchronized (bucket) { - servers.add(bucket.getPrimaryBackup()); - servers.add(bucket.getSecondaryBackup()); - } - sendBucketToBackupServers(bucket, count, entity, servers); - } - } - } catch (Exception e) { - logger.error("Persistence.PersistenceRunnable.run:", e); - } - } - - private void sendBucketToBackupServers(Bucket bucket, final int count, final Entity<String> entity, - Set<Server> servers) { - - for (final Server server : servers) { - if (server != null) { - // send out REST command - server.getThreadPool().execute(() -> { - WebTarget webTarget = - server.getWebTarget("persistence/session"); - if (webTarget != null) { - webTarget - .queryParam(QP_BUCKET, - bucket.getIndex()) - .queryParam(QP_SESSION, - encodedSessionName) - .queryParam(QP_COUNT, count) - .queryParam(QP_DEST, server.getUuid()) - .request().post(entity); - } - }); - } - } - } - - /* ************************************** */ - /* 'RuleRuntimeEventListener' interface */ - /* ************************************** */ - - /** - * {@inheritDoc} - */ - @Override - public void objectDeleted(ObjectDeletedEvent event) { - // determine Drools object that was deleted - Object object = event.getOldObject(); - - // determine keyword, if any - String keyword = Keyword.lookupKeyword(object); - if (keyword == null) { - // no keyword, so there is no associated bucket - return; - } - - // locate bucket and associated data - // (don't create adjunct if it isn't there -- there's nothing to delete) - Bucket bucket = Bucket.getBucket(keyword); - SenderBucketData sbd = - bucket.getAdjunctDontCreate(SenderBucketData.class); - if (sbd != null) { - // add bucket to 'modified' list - synchronized (modifiedBuckets) { - modifiedBuckets.add(bucket); - } - - // update set of Drools objects in this bucket - sbd.getSessionData(session).objectDeleted(object); - - // insert this 'DroolsRunnable' to do the backup (note that it - // may already be inserted from a previous update to this - // DroolsSession -- eventually, the rule will fire, and the 'run' - // method will be called) - session.getKieSession().insert(this); - } - } - - /** - * {@inheritDoc} - */ - @Override - public void objectInserted(ObjectInsertedEvent event) { - objectChanged(event.getObject()); - } - - /** - * {@inheritDoc} - */ - @Override - public void objectUpdated(ObjectUpdatedEvent event) { - objectChanged(event.getObject()); - } - - /** - * A Drools session object was either inserted or updated - * (both are treated the same way). - * - * @param object the object being inserted or updated - */ - private void objectChanged(Object object) { - // determine keyword, if any - String keyword = Keyword.lookupKeyword(object); - if (keyword == null) { - // no keyword, so there is no associated bucket - return; - } - - // locate bucket and associated data - Bucket bucket = Bucket.getBucket(keyword); - SenderBucketData sbd = bucket.getAdjunct(SenderBucketData.class); - - // add bucket to 'modified' list - synchronized (modifiedBuckets) { - modifiedBuckets.add(bucket); - } - - // update set of Drools objects in this bucket - sbd.getSessionData(session).objectChanged(object); - - // insert this 'DroolsRunnable' to do the backup (note that it - // may already be inserted from a previous update to this - // DroolsSession -- eventually, the rule will fire, and the 'run' - // method will be called) - session.getKieSession().insert(this); - } - } - - /* ============================================================ */ - - /** - * Per-session data for a single bucket on the sender's side. - */ - static class SenderSessionBucketData { - // the set of all objects in the session associated with this bucket - Map<Object, Object> droolsObjects = new IdentityHashMap<>(); - - // used by the receiver to determine whether an update is really newer - int count = 0; - - // serialized base64 form of 'droolsObjects' - // (TBD: determine if we are getting any benefit from caching this) - byte[] encodedSerializedData = null; - - // 'true' means that 'encodedSerializedData' is out-of-date - boolean rebuildNeeded = true; - - /** - * Notification that a Drools object associated with this bucket - * was deleted. - * - * @param object the object that was deleted - */ - synchronized void objectDeleted(Object object) { - if (droolsObjects.remove(object) != null) { - rebuildNeeded = true; - } - } - - /** - * Notification that a Drools object associated with this bucket - * was inserted or updated. - * - * @param object the object that was updated - */ - synchronized void objectChanged(Object object) { - droolsObjects.put(object, object); - rebuildNeeded = true; - } - - /** - * Serialize and base64-encode the objects in this Drools session. - * - * @return a byte array containing the encoded serialized objects - */ - synchronized byte[] getLatestEncodedSerializedData() { - if (rebuildNeeded) { - try { - // this should be run in the Drools session thread in order - // to avoid transient data - encodedSerializedData = - Base64.getEncoder().encode(Util.serialize(droolsObjects)); - count += 1; - } catch (IOException e) { - logger.error("Persistence.SenderSessionBucketData." - + "getLatestEncodedSerializedData: ", e); - } - rebuildNeeded = false; - } - return encodedSerializedData; - } - - /** - * Return a counter that will be used for update comparison. - * - * @return the value of a counter that can be used to determine whether - * an update is really newer than the previous update - */ - synchronized int getCount() { - return count; - } - } - - /* ============================================================ */ - - /** - * Data for a single bucket on the sender's side. - */ - public static class SenderBucketData { - // maps session name into SenderSessionBucketData - Map<PolicySession, SenderSessionBucketData> sessionData = - new IdentityHashMap<>(); - - // used by the receiver to determine whether an update is really newer - int lockCount = 0; - - /** - * Create or fetch the 'SenderSessionBucketData' instance associated - * with the specified session. - * - * @param session the 'PolicySession' object - * @return the associated 'SenderSessionBucketData' instance - */ - synchronized SenderSessionBucketData getSessionData(PolicySession session) { - return sessionData.computeIfAbsent(session, key -> new SenderSessionBucketData()); - } - - /** - * Return a counter that will be used for update comparison. - * - * @return the value of a counter that can be used to determine whether - * an update is really newer than the previous update - */ - int getLockCountAndIncrement() { - // note that this is synchronized using the 'GlobalLocks' instance - // within the same bucket - return lockCount++; - } - } - - /* ============================================================ */ - - /** - * Data for a single bucket and session on the receiver's side. - */ - static class ReceiverSessionBucketData { - // used to determine whether an update is really newer - int count = -1; - - // serialized base64 form of 'droolsObjects' - byte[] encodedSerializedData = null; - } - - /* ============================================================ */ - - /** - * Data for a single bucket on the receiver's side -- this adjunct is used - * to store encoded data on a backup host. It will only be needed if the - * bucket owner fails. - */ - public static class ReceiverBucketData { - static final String RESTORE_BUCKET_ERROR = - "Persistence.ReceiverBucketData.restoreBucket: "; - - // maps session name into encoded data - Map<String, ReceiverSessionBucketData> sessionData = new HashMap<>(); - - // used by the receiver to determine whether an update is really newer - int lockCount = -1; - - // encoded lock data - byte[] lockData = null; - - /** - * This method is called in response to the '/persistence/session' - * REST message. It stores the base64-encoded and serialized data - * for a particular bucket and session. - * - * @param bucketNumber identifies the bucket - * @param sessionName identifies the Drools session - * @param count counter used to determine whether data is really newer - * @param data base64-encoded serialized data for this bucket and session - */ - static void receiveSession(int bucketNumber, String sessionName, int count, byte[] data) { - // fetch the bucket - Bucket bucket = Bucket.getBucket(bucketNumber); - - // create/fetch the 'ReceiverBucketData' adjunct - ReceiverBucketData rbd = bucket.getAdjunct(ReceiverBucketData.class); - synchronized (rbd) { - // update the session data - ReceiverSessionBucketData rsbd = rbd.sessionData.get(sessionName); - if (rsbd == null) { - rsbd = new ReceiverSessionBucketData(); - rbd.sessionData.put(sessionName, rsbd); - } - - if ((count - rsbd.count) > 0 || count == 0) { - // this is new data - rsbd.count = count; - rsbd.encodedSerializedData = data; - } - } - } - - /** - * This method is called in response to the '/persistence/lock' - * REST message. It stores the base64-encoded and serialized - * server-side lock data associated with this bucket. - * - * @param bucketNumber identifies the bucket - * @param count counter used to determine whether data is really newer - * @param data base64-encoded serialized lock data for this bucket - */ - static void receiveLockData(int bucketNumber, int count, byte[] data) { - // fetch the bucket - Bucket bucket = Bucket.getBucket(bucketNumber); - - // create/fetch the 'ReceiverBucketData' adjunct - ReceiverBucketData rbd = bucket.getAdjunct(ReceiverBucketData.class); - synchronized (rbd) { - // update the lock data - if ((count - rbd.lockCount) > 0 || count == 0) { - rbd.lockCount = count; - rbd.lockData = data; - } - } - } - - /** - * This method is called when a bucket is being restored from persistent - * data, which indicates that a clean migration didn't occur. - * Drools session and/or lock data is restored. - * - * @param bucket the bucket being restored - */ - synchronized void restoreBucket(Bucket bucket) { - // one entry for each Drools session being restored -- - // indicates when the restore is complete (restore runs within - // the Drools session thread) - List<CountDownLatch> sessionLatches = restoreBucketDroolsSessions(); - - // restore lock data - restoreBucketLocks(bucket); - - // wait for all of the sessions to update - try { - for (CountDownLatch sessionLatch : sessionLatches) { - if (!sessionLatch.await(10000L, TimeUnit.MILLISECONDS)) { - logger.error("{}: timed out waiting for session latch", - this); - } - } - } catch (InterruptedException e) { - logger.error("Exception in {}", this, e); - Thread.currentThread().interrupt(); - } - } - - private List<CountDownLatch> restoreBucketDroolsSessions() { - List<CountDownLatch> sessionLatches = new LinkedList<>(); - for (Map.Entry<String, ReceiverSessionBucketData> entry : sessionData.entrySet()) { - restoreBucketDroolsSession(sessionLatches, entry); - } - return sessionLatches; - } - - private void restoreBucketDroolsSession(List<CountDownLatch> sessionLatches, - Entry<String, ReceiverSessionBucketData> entry) { - - String sessionName = entry.getKey(); - ReceiverSessionBucketData rsbd = entry.getValue(); - - PolicySession policySession = detmPolicySession(sessionName); - if (policySession == null) { - logger.error(RESTORE_BUCKET_ERROR - + "Can't find PolicySession{}", sessionName); - return; - } - - final Map<?, ?> droolsObjects = deserializeMap(sessionName, rsbd, policySession); - if (droolsObjects == null) { - return; - } - - // if we reach this point, we have decoded the persistent data - - // signal when restore is complete - final CountDownLatch sessionLatch = new CountDownLatch(1); - - // 'KieSession' object - final KieSession kieSession = policySession.getKieSession(); - - // run the following within the Drools session thread - DroolsRunnable insertDroolsObjects = () -> { - try { - // insert all of the Drools objects into the session - for (Object droolsObj : droolsObjects.keySet()) { - kieSession.insert(droolsObj); - } - } finally { - // signal completion - sessionLatch.countDown(); - } - }; - kieSession.insert(insertDroolsObjects); - - // add this to the set of 'CountDownLatch's we are waiting for - sessionLatches.add(sessionLatch); - } - - private PolicySession detmPolicySession(String sessionName) { - // [0]="<groupId>" [1]="<artifactId>", [2]="<sessionName>" - String[] nameSegments = sessionName.split(":"); - - // locate the 'PolicyContainer' and 'PolicySession' - if (nameSegments.length == 3) { - // step through all 'PolicyContainer' instances looking - // for a matching 'artifactId' & 'groupId' - for (PolicyContainer pc : PolicyContainer.getPolicyContainers()) { - if (nameSegments[1].equals(pc.getArtifactId()) - && nameSegments[0].equals(pc.getGroupId())) { - // 'PolicyContainer' matches -- try to fetch the session - return pc.getPolicySession(nameSegments[2]); - } - } - } - return null; - } - - private Map<?, ?> deserializeMap(String sessionName, ReceiverSessionBucketData rsbd, - PolicySession policySession) { - Object obj; - - try { - // deserialization needs to use the correct 'ClassLoader' - obj = Util.deserialize(Base64.getDecoder().decode(rsbd.encodedSerializedData), - policySession.getPolicyContainer().getClassLoader()); - } catch (IOException | ClassNotFoundException | IllegalArgumentException e) { - logger.error(RESTORE_BUCKET_ERROR - + "Failed to read data for session '{}'", - sessionName, e); - - // can't decode -- skip this session - return null; - } - - if (!(obj instanceof Map)) { - logger.error(RESTORE_BUCKET_ERROR - + "Session '{}' data has class {}, expected 'Map'", - sessionName, obj.getClass().getName()); - - // wrong object type decoded -- skip this session - return null; - } - - // if we reach this point, we have decoded the persistent data - - return (Map<?, ?>) obj; - } - - private void restoreBucketLocks(Bucket bucket) { - if (lockData != null) { - Object obj = null; - try { - // decode lock data - obj = Util.deserialize(Base64.getDecoder().decode(lockData)); - if (obj instanceof GlobalLocks) { - bucket.putAdjunct(obj); - - // send out updated date - sendLockDataToBackups(bucket, (GlobalLocks) obj); - } else { - logger.error(RESTORE_BUCKET_ERROR - + "Expected 'GlobalLocks', got '{}'", - obj.getClass().getName()); - } - } catch (IOException | ClassNotFoundException | IllegalArgumentException e) { - logger.error(RESTORE_BUCKET_ERROR - + "Failed to read lock data", e); - // skip the lock data - } - - } - } - } - - /* ============================================================ */ - - @Path("/") - public static class Rest { - /** - * Handle the '/persistence/session' REST call. - */ - @POST - @Path("/persistence/session") - @Consumes(MediaType.APPLICATION_OCTET_STREAM) - public void receiveSession(@QueryParam(QP_BUCKET) int bucket, - @QueryParam(QP_SESSION) String sessionName, - @QueryParam(QP_COUNT) int count, - @QueryParam(QP_DEST) UUID dest, - byte[] data) { - logger.debug("/persistence/session: (bucket={},session={},count={}) " - + "got {} bytes of data", - bucket, sessionName, count, data.length); - if (dest == null || dest.equals(Server.getThisServer().getUuid())) { - ReceiverBucketData.receiveSession(bucket, sessionName, count, data); - } else { - // This host is not the intended destination -- this could happen - // if it was sent from another site. Leave off the 'dest' param - // when forwarding the message, to ensure that we don't have - // an infinite forwarding loop, if the site data happens to be bad. - Server server; - WebTarget webTarget; - - if ((server = Server.getServer(dest)) != null - && (webTarget = - server.getWebTarget("persistence/session")) != null) { - Entity<String> entity = - Entity.entity(new String(data), - MediaType.APPLICATION_OCTET_STREAM_TYPE); - webTarget - .queryParam(QP_BUCKET, bucket) - .queryParam(QP_SESSION, sessionName) - .queryParam(QP_COUNT, count) - .request().post(entity); - } - } - } - - /** - * Handle the '/persistence/lock' REST call. - */ - @POST - @Path("/persistence/lock") - @Consumes(MediaType.APPLICATION_OCTET_STREAM) - public void receiveLockData(@QueryParam(QP_BUCKET) int bucket, - @QueryParam(QP_COUNT) int count, - @QueryParam(QP_DEST) UUID dest, - byte[] data) { - logger.debug("/persistence/lock: (bucket={},count={}) " - + "got {} bytes of data", bucket, count, data.length); - if (dest == null || dest.equals(Server.getThisServer().getUuid())) { - ReceiverBucketData.receiveLockData(bucket, count, data); - } else { - // This host is not the intended destination -- this could happen - // if it was sent from another site. Leave off the 'dest' param - // when forwarding the message, to ensure that we don't have - // an infinite forwarding loop, if the site data happens to be bad. - Server server; - WebTarget webTarget; - - if ((server = Server.getServer(dest)) != null - && (webTarget = server.getWebTarget("persistence/lock")) != null) { - Entity<String> entity = - Entity.entity(new String(data), - MediaType.APPLICATION_OCTET_STREAM_TYPE); - webTarget - .queryParam(QP_BUCKET, bucket) - .queryParam(QP_COUNT, count) - .request().post(entity); - } - } - } - } -} diff --git a/feature-server-pool/src/main/resources/META-INF/services/org.onap.policy.drools.control.api.DroolsPdpStateControlApi b/feature-server-pool/src/main/resources/META-INF/services/org.onap.policy.drools.control.api.DroolsPdpStateControlApi deleted file mode 100644 index 3dc6a574..00000000 --- a/feature-server-pool/src/main/resources/META-INF/services/org.onap.policy.drools.control.api.DroolsPdpStateControlApi +++ /dev/null @@ -1 +0,0 @@ -org.onap.policy.drools.serverpool.FeatureServerPool diff --git a/feature-server-pool/src/main/resources/META-INF/services/org.onap.policy.drools.core.PolicySessionFeatureApi b/feature-server-pool/src/main/resources/META-INF/services/org.onap.policy.drools.core.PolicySessionFeatureApi deleted file mode 100644 index 8ad5a18f..00000000 --- a/feature-server-pool/src/main/resources/META-INF/services/org.onap.policy.drools.core.PolicySessionFeatureApi +++ /dev/null @@ -1,2 +0,0 @@ -org.onap.policy.drools.serverpool.FeatureServerPool -org.onap.policy.drools.serverpool.persistence.Persistence diff --git a/feature-server-pool/src/main/resources/META-INF/services/org.onap.policy.drools.features.PolicyControllerFeatureApi b/feature-server-pool/src/main/resources/META-INF/services/org.onap.policy.drools.features.PolicyControllerFeatureApi deleted file mode 100644 index 3dc6a574..00000000 --- a/feature-server-pool/src/main/resources/META-INF/services/org.onap.policy.drools.features.PolicyControllerFeatureApi +++ /dev/null @@ -1 +0,0 @@ -org.onap.policy.drools.serverpool.FeatureServerPool diff --git a/feature-server-pool/src/main/resources/META-INF/services/org.onap.policy.drools.features.PolicyEngineFeatureApi b/feature-server-pool/src/main/resources/META-INF/services/org.onap.policy.drools.features.PolicyEngineFeatureApi deleted file mode 100644 index 3dc6a574..00000000 --- a/feature-server-pool/src/main/resources/META-INF/services/org.onap.policy.drools.features.PolicyEngineFeatureApi +++ /dev/null @@ -1 +0,0 @@ -org.onap.policy.drools.serverpool.FeatureServerPool diff --git a/feature-server-pool/src/main/resources/META-INF/services/org.onap.policy.drools.serverpool.ServerPoolApi b/feature-server-pool/src/main/resources/META-INF/services/org.onap.policy.drools.serverpool.ServerPoolApi deleted file mode 100644 index a72d8cb2..00000000 --- a/feature-server-pool/src/main/resources/META-INF/services/org.onap.policy.drools.serverpool.ServerPoolApi +++ /dev/null @@ -1 +0,0 @@ -org.onap.policy.drools.serverpool.persistence.Persistence |