From 5be5b1915b9296c2da737b55374176c7883f2a64 Mon Sep 17 00:00:00 2001 From: Areek Zillur Date: Wed, 21 Jan 2015 14:40:09 -0500 Subject: [PATCH] Add support for License Expiration event triggers This enhancement allows consumer plugins to configure event notifications from the licensing plugin relative to its license expiry. Original commit: elastic/x-pack-elasticsearch@11b53dd78de1fa6b909e451d6b6a19d4539f5947 --- core-shaded/pom.xml | 2 +- core/pom.xml | 4 +- .../elasticsearch/license/core/Licenses.java | 2 +- licensor/pom.xml | 4 +- plugin/pom.xml | 6 +- .../plugin/core/LicensesClientService.java | 12 +- .../license/plugin/core/LicensesService.java | 272 +++++++++++++++--- .../AbstractLicensesIntegrationTests.java | 6 +- .../plugin/AbstractLicensesServiceTests.java | 43 ++- .../plugin/LicensesClientServiceTests.java | 224 +++++++++++++-- .../plugin/LicensesManagerServiceTests.java | 2 +- .../LicensesPluginIntegrationTests.java | 2 +- .../plugin/LicensesTransportTests.java | 2 +- .../consumer/TestPluginServiceBase.java | 48 +++- pom.xml | 2 +- 15 files changed, 541 insertions(+), 90 deletions(-) diff --git a/core-shaded/pom.xml b/core-shaded/pom.xml index 3472a124539..6be3c61bda4 100644 --- a/core-shaded/pom.xml +++ b/core-shaded/pom.xml @@ -5,7 +5,7 @@ elasticsearch-license org.elasticsearch - 1.0.0-beta2 + 1.0.0-SNAPSHOT 4.0.0 diff --git a/core/pom.xml b/core/pom.xml index 443f61ef928..ff0f2322a61 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -5,7 +5,7 @@ elasticsearch-license org.elasticsearch - 1.0.0-beta2 + 1.0.0-SNAPSHOT 4.0.0 @@ -17,7 +17,7 @@ org.elasticsearch elasticsearch-license-core-shaded - 1.0.0-beta2 + 1.0.0-SNAPSHOT diff --git a/core/src/main/java/org/elasticsearch/license/core/Licenses.java b/core/src/main/java/org/elasticsearch/license/core/Licenses.java index d4076be380d..efb1d884e7b 100644 --- a/core/src/main/java/org/elasticsearch/license/core/Licenses.java +++ b/core/src/main/java/org/elasticsearch/license/core/Licenses.java @@ -142,7 +142,7 @@ public final class Licenses { if (license.expiryDate() > previousLicense.expiryDate()) { licenseMap.put(featureType, license); } - } else if (license.expiryDate() > System.currentTimeMillis()) { + } else { licenseMap.put(featureType, license); } } diff --git a/licensor/pom.xml b/licensor/pom.xml index 5bfc414f8c4..b2a4e328367 100644 --- a/licensor/pom.xml +++ b/licensor/pom.xml @@ -5,7 +5,7 @@ elasticsearch-license org.elasticsearch - 1.0.0-beta2 + 1.0.0-SNAPSHOT 4.0.0 @@ -19,7 +19,7 @@ org.elasticsearch elasticsearch-license-core - 1.0.0-beta2 + 1.0.0-SNAPSHOT diff --git a/plugin/pom.xml b/plugin/pom.xml index 63618de5054..38f648e37be 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -5,7 +5,7 @@ elasticsearch-license org.elasticsearch - 1.0.0-beta2 + 1.0.0-SNAPSHOT 4.0.0 @@ -21,13 +21,13 @@ org.elasticsearch elasticsearch-license-licensor - 1.0.0-beta2 + 1.0.0-SNAPSHOT test org.elasticsearch elasticsearch-license-core - 1.0.0-beta2 + 1.0.0-SNAPSHOT compile diff --git a/plugin/src/main/java/org/elasticsearch/license/plugin/core/LicensesClientService.java b/plugin/src/main/java/org/elasticsearch/license/plugin/core/LicensesClientService.java index 54a5ae73b4e..eb289cdb0e4 100644 --- a/plugin/src/main/java/org/elasticsearch/license/plugin/core/LicensesClientService.java +++ b/plugin/src/main/java/org/elasticsearch/license/plugin/core/LicensesClientService.java @@ -6,7 +6,11 @@ package org.elasticsearch.license.plugin.core; import org.elasticsearch.common.inject.ImplementedBy; +import org.elasticsearch.license.core.License; +import java.util.Collection; + +import static org.elasticsearch.license.plugin.core.LicensesService.*; import static org.elasticsearch.license.plugin.core.LicensesService.TrialLicenseOptions; @ImplementedBy(LicensesService.class) @@ -17,12 +21,13 @@ public interface LicensesClientService { /** * Called to enable a feature */ - public void onEnabled(); + public void onEnabled(License license); /** * Called to disable a feature */ - public void onDisabled(); + public void onDisabled(License license); + } /** @@ -31,7 +36,8 @@ public interface LicensesClientService { * @param feature - name of the feature to register (must be in sync with license Generator feature name) * @param trialLicenseOptions - Trial license specification used to generate a one-time trial license for the feature; * use null if no trial license should be generated for the feature + * @param expirationCallbacks - A collection of Pre and/or Post expiration callbacks * @param listener - used to notify on feature enable/disable */ - void register(String feature, TrialLicenseOptions trialLicenseOptions, Listener listener); + void register(String feature, TrialLicenseOptions trialLicenseOptions, Collection expirationCallbacks, Listener listener); } diff --git a/plugin/src/main/java/org/elasticsearch/license/plugin/core/LicensesService.java b/plugin/src/main/java/org/elasticsearch/license/plugin/core/LicensesService.java index 0776ce5f838..032a747363d 100644 --- a/plugin/src/main/java/org/elasticsearch/license/plugin/core/LicensesService.java +++ b/plugin/src/main/java/org/elasticsearch/license/plugin/core/LicensesService.java @@ -36,11 +36,9 @@ import org.elasticsearch.transport.*; import java.io.IOException; import java.util.*; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import static org.elasticsearch.license.core.Licenses.reduceAndMap; @@ -54,7 +52,7 @@ import static org.elasticsearch.license.core.Licenses.reduceAndMap; *

* Registration Scheme: *

- * A consumer plugin (feature) is registered with {@link LicensesClientService#register(String, TrialLicenseOptions, LicensesClientService.Listener)} + * A consumer plugin (feature) is registered with {@link LicensesClientService#register(String, TrialLicenseOptions, java.util.Collection, LicensesClientService.Listener)} * This method can be called at any time during the life-cycle of the consumer plugin. * If the feature can not be registered immediately, it is queued up and registered on the first clusterChanged event with * no {@link org.elasticsearch.gateway.GatewayService#STATE_NOT_RECOVERED_BLOCK} block @@ -112,6 +110,11 @@ public class LicensesService extends AbstractLifecycleComponent */ private final Queue scheduledNotifications = new ConcurrentLinkedQueue<>(); + /** + * Currently active event notifications for every registered feature + */ + private final Map> eventNotificationsMap = new HashMap<>(); + /** * The last licensesMetaData that has been notified by {@link #notifyFeatures(LicensesMetaData)} */ @@ -352,9 +355,18 @@ public class LicensesService extends AbstractLifecycleComponent scheduledNotification.cancel(true); } + for (Queue queue : eventNotificationsMap.values()) { + for (ScheduledFuture scheduledFuture : queue) { + scheduledFuture.cancel(true); + } + queue.clear(); + } + + LicensesMetaData currentLicensesMetaData = clusterService.state().metaData().custom(LicensesMetaData.TYPE); + final Map effectiveLicenses = getEffectiveLicenses(currentLicensesMetaData); // notify features to be disabled for (ListenerHolder holder : registeredListeners) { - holder.disableFeatureIfNeeded(false); + holder.disableFeatureIfNeeded(effectiveLicenses.get(holder.feature), false); } // clear all handlers registeredListeners.clear(); @@ -410,6 +422,10 @@ public class LicensesService extends AbstractLifecycleComponent } else { notifyFeaturesAndScheduleNotificationIfNeeded(currentLicensesMetaData); } + final Map effectiveLicenses = getEffectiveLicenses(currentLicensesMetaData); + for (ListenerHolder listenerHolder : registeredListeners) { + listenerHolder.scheduleNotificationIfNeeded(effectiveLicenses.get(listenerHolder.feature)); + } } else if (logger.isDebugEnabled()) { logger.debug("clusterChanged: no action [has STATE_NOT_RECOVERED_BLOCK]"); } @@ -457,20 +473,25 @@ public class LicensesService extends AbstractLifecycleComponent private long notifyFeatures(final LicensesMetaData currentLicensesMetaData) { long nextScheduleFrequency = -1l; for (ListenerHolder listenerHolder : registeredListeners) { - long expiryDate = expiryDateForFeature(listenerHolder.feature, currentLicensesMetaData); - long issueDate = issueDateForFeature(listenerHolder.feature, currentLicensesMetaData); + final Map effectiveLicenses = getEffectiveLicenses(currentLicensesMetaData); + License license = effectiveLicenses.get(listenerHolder.feature); + if (license == null) { + continue; + } + long expiryDate = license.expiryDate(); + long issueDate = license.issueDate(); long now = System.currentTimeMillis(); long expiryDuration = expiryDate - now; if (expiryDuration > 0l && (now - issueDate) >= 0l) { - listenerHolder.enableFeatureIfNeeded(); + listenerHolder.enableFeatureIfNeeded(license); if (nextScheduleFrequency == -1l) { nextScheduleFrequency = expiryDuration; } else { nextScheduleFrequency = Math.min(expiryDuration, nextScheduleFrequency); } } else { - listenerHolder.disableFeatureIfNeeded(true); + listenerHolder.disableFeatureIfNeeded(license, true); } if (logger.isDebugEnabled()) { @@ -529,13 +550,15 @@ public class LicensesService extends AbstractLifecycleComponent * {@inheritDoc} */ @Override - public void register(String feature, TrialLicenseOptions trialLicenseOptions, Listener listener) { + public void register(String feature, TrialLicenseOptions trialLicenseOptions, Collection expirationCallbacks, Listener listener) { for (final ListenerHolder listenerHolder : Iterables.concat(registeredListeners, pendingListeners)) { if (listenerHolder.feature.equals(feature)) { throw new IllegalStateException("feature: [" + feature + "] has been already registered"); } } - final ListenerHolder listenerHolder = new ListenerHolder(feature, trialLicenseOptions, listener); + Queue notificationQueue = new ConcurrentLinkedQueue<>(); + eventNotificationsMap.put(feature, notificationQueue); + final ListenerHolder listenerHolder = new ListenerHolder(feature, trialLicenseOptions, expirationCallbacks, listener, notificationQueue); // don't trust the clusterState for blocks just yet! final Lifecycle.State clusterServiceState = clusterService.lifecycleState(); if (clusterServiceState != Lifecycle.State.STARTED) { @@ -581,8 +604,9 @@ public class LicensesService extends AbstractLifecycleComponent } else { // could not sent register trial license request to master logger.debug("Store as pendingRegistration [master not available yet]"); - return false; } + // make sure trial license is available before registration + return false; } } else if (logger.isDebugEnabled()) { // notify feature as clusterChangedEvent may not happen @@ -594,21 +618,18 @@ public class LicensesService extends AbstractLifecycleComponent // feature, notify feature on registration logger.debug("Calling notifyFeaturesAndScheduleNotification [signed/trial license available]"); } + + if (currentMetaData == null) { + return false; + } registeredListeners.add(listenerHolder); notifyFeaturesAndScheduleNotification(currentMetaData); + final Map effectiveLicenses = getEffectiveLicenses(currentMetaData); + listenerHolder.scheduleNotificationIfNeeded(effectiveLicenses.get(listenerHolder.feature)); return true; } - private long issueDateForFeature(String feature, final LicensesMetaData currentLicensesMetaData) { - final Map effectiveLicenses = getEffectiveLicenses(currentLicensesMetaData); - License featureLicense; - if ((featureLicense = effectiveLicenses.get(feature)) != null) { - return featureLicense.issueDate(); - } - return -1l; - } - - private long expiryDateForFeature(String feature, final LicensesMetaData currentLicensesMetaData) { + private static long expiryDateForFeature(String feature, final LicensesMetaData currentLicensesMetaData) { final Map effectiveLicenses = getEffectiveLicenses(currentLicensesMetaData); License featureLicense; if ((featureLicense = effectiveLicenses.get(feature)) != null) { @@ -617,7 +638,7 @@ public class LicensesService extends AbstractLifecycleComponent return -1l; } - private Map getEffectiveLicenses(final LicensesMetaData metaData) { + private static Map getEffectiveLicenses(final LicensesMetaData metaData) { Map map = new HashMap<>(); if (metaData != null) { Set licenses = new HashSet<>(); @@ -634,10 +655,9 @@ public class LicensesService extends AbstractLifecycleComponent } /** - * Clears out any completed notification future from - * {@link #scheduledNotifications} + * Clears out any completed notification futures */ - private void clearFinishedNotifications() { + private static void clearFinishedNotifications(Queue scheduledNotifications) { while (!scheduledNotifications.isEmpty()) { ScheduledFuture notification = scheduledNotifications.peek(); if (notification != null && notification.isDone()) { @@ -658,7 +678,7 @@ public class LicensesService extends AbstractLifecycleComponent * Schedules an expiry notification with a delay of nextScheduleDelay */ private void scheduleNextNotification(long nextScheduleDelay) { - clearFinishedNotifications(); + clearFinishedNotifications(scheduledNotifications); try { final TimeValue delay = TimeValue.timeValueMillis(nextScheduleDelay); @@ -697,7 +717,6 @@ public class LicensesService extends AbstractLifecycleComponent } } - public static class PutLicenseRequestHolder { private final PutLicenseRequest request; private final String source; @@ -728,38 +747,223 @@ public class LicensesService extends AbstractLifecycleComponent } } + public static class ExpirationStatus { + private final boolean expired; + private final TimeValue time; + + private ExpirationStatus(boolean expired, TimeValue time) { + this.expired = expired; + this.time = time; + } + + public boolean expired() { + return expired; + } + + public TimeValue time() { + return time; + } + } + + public static interface LicenseCallback { + void on(License license, ExpirationStatus status); + } + + public static abstract class ExpirationCallback implements LicenseCallback { + + public enum Orientation { PRE, POST } + + public static abstract class Pre extends ExpirationCallback { + + /** + * Callback schedule prior to license expiry + * + * @param min latest relative time to execute before license expiry + * @param max earliest relative time to execute before license expiry + * @param frequency interval between execution + */ + public Pre(TimeValue min, TimeValue max, TimeValue frequency) { + super(Orientation.PRE, min, max, frequency); + } + + @Override + public boolean matches(long expirationDate, long now) { + long expiryDuration = expirationDate - now; + if (expiryDuration > 0l) { + if (expiryDuration <= max().getMillis()) { + return expiryDuration >= min().getMillis(); + } + } + return false; + } + } + + public static abstract class Post extends ExpirationCallback { + + /** + * Callback schedule after license expiry + * + * @param min earliest relative time to execute after license expiry + * @param max latest relative time to execute after license expiry + * @param frequency interval between execution + */ + public Post(TimeValue min, TimeValue max, TimeValue frequency) { + super(Orientation.POST, min, max, frequency); + } + + @Override + public boolean matches(long expirationDate, long now) { + long postExpiryDuration = now - expirationDate; + if (postExpiryDuration > 0l) { + if (postExpiryDuration <= max().getMillis()) { + return postExpiryDuration >= min().getMillis(); + } + } + return false; + } + } + + private final Orientation orientation; + private final TimeValue min; + private final TimeValue max; + private final TimeValue frequency; + + private ExpirationCallback(Orientation orientation, TimeValue min, TimeValue max, TimeValue frequency) { + this.orientation = orientation; + this.min = (min == null) ? TimeValue.timeValueMillis(0) : min; + this.max = (max == null) ? TimeValue.timeValueMillis(Long.MAX_VALUE) : max; + this.frequency = frequency; + if (frequency == null) { + throw new IllegalArgumentException("frequency can not be null"); + } + } + + public Orientation orientation() { + return orientation; + } + + public TimeValue min() { + return min; + } + + public TimeValue max() { + return max; + } + + public TimeValue frequency() { + return frequency; + } + + public abstract boolean matches(long expirationDate, long now); + } + /** * Stores configuration and listener for a feature */ private class ListenerHolder { final String feature; final TrialLicenseOptions trialLicenseOptions; + final Collection expirationCallbacks; final Listener listener; + final AtomicLong currentExpiryDate = new AtomicLong(-1l); + final Queue notificationQueue; volatile AtomicBoolean enabled = new AtomicBoolean(false); // by default, a consumer plugin should be disabled - private ListenerHolder(String feature, TrialLicenseOptions trialLicenseOptions, Listener listener) { + private ListenerHolder(String feature, TrialLicenseOptions trialLicenseOptions, Collection expirationCallbacks, Listener listener, Queue notificationQueue) { this.feature = feature; this.trialLicenseOptions = trialLicenseOptions; + this.expirationCallbacks = expirationCallbacks; this.listener = listener; + this.notificationQueue = notificationQueue; } - private void enableFeatureIfNeeded() { + private void enableFeatureIfNeeded(License license) { if (enabled.compareAndSet(false, true)) { - listener.onEnabled(); + listener.onEnabled(license); logger.info("license for [" + feature + "] - valid"); } } - private void disableFeatureIfNeeded(boolean log) { + private void disableFeatureIfNeeded(License license, boolean log) { if (enabled.compareAndSet(true, false)) { - listener.onDisabled(); + listener.onDisabled(license); if (log) { logger.info("license for [" + feature + "] - expired"); } } } + private Runnable triggerJob() { + return new Runnable() { + @Override + public void run() { + LicensesMetaData currentLicensesMetaData = clusterService.state().metaData().custom(LicensesMetaData.TYPE); + final Map effectiveLicenses = getEffectiveLicenses(currentLicensesMetaData); + triggerEvents(effectiveLicenses.get(feature), System.currentTimeMillis()); + } + }; + } + + private void scheduleNotificationIfNeeded(final License license) { + long expiryDate = ((license != null) ? license.expiryDate() : -1l); + if (currentExpiryDate.get() == expiryDate) { + return; + } + currentExpiryDate.set(expiryDate); + + // clear out notification queue + while (!notificationQueue.isEmpty()) { + ScheduledFuture notification = notificationQueue.peek(); + if (notification != null) { + // cancel + notification.cancel(true); + notificationQueue.poll(); + } + } + + long now = System.currentTimeMillis(); + // Schedule the first for all the callbacks + long expiryDuration = expiryDate - now; + + //schedule first event of callbacks that will be activated in the future + for (ExpirationCallback expirationCallback : expirationCallbacks) { + if (!expirationCallback.matches(expiryDate, now)) { + long delay = -1l; + switch (expirationCallback.orientation()) { + case PRE: + delay = expiryDuration - expirationCallback.max().getMillis(); + break; + case POST: + if (expiryDuration > 0l) { + delay = expiryDuration + expirationCallback.min().getMillis(); + } else { + delay = (-1l * expiryDuration) - expirationCallback.min().getMillis(); + } + break; + } + if (delay > 0l) { + notificationQueue.add(threadPool.schedule(TimeValue.timeValueMillis(delay), executorName(), triggerJob())); + } + } + } + if (license != null) { + // schedule first event of callbacks that match + triggerEvents(license, now); + } + } + + private void triggerEvents(final License license, long now) { + for (ExpirationCallback expirationCallback : expirationCallbacks) { + if (expirationCallback.matches(license.expiryDate(), now)) { + long expiryDuration = license.expiryDate() - now; + boolean expired = expiryDuration <= 0l; + expirationCallback.on(license, new ExpirationStatus(expired, TimeValue.timeValueMillis((!expired) ? expiryDuration : (-1l * expiryDuration)))); + notificationQueue.add(threadPool.schedule(expirationCallback.frequency(), executorName(), triggerJob())); + } + } + } + public String toString() { return "(feature: " + feature + ", enabled: " + enabled.get() + ")"; } diff --git a/plugin/src/test/java/org/elasticsearch/license/plugin/AbstractLicensesIntegrationTests.java b/plugin/src/test/java/org/elasticsearch/license/plugin/AbstractLicensesIntegrationTests.java index a5e257525f8..310deda38f9 100644 --- a/plugin/src/test/java/org/elasticsearch/license/plugin/AbstractLicensesIntegrationTests.java +++ b/plugin/src/test/java/org/elasticsearch/license/plugin/AbstractLicensesIntegrationTests.java @@ -152,7 +152,7 @@ public abstract class AbstractLicensesIntegrationTests extends ElasticsearchInte } private void assertConsumerPluginNotification(String msg, final Iterable consumerPluginServices, final boolean expectedEnabled, int timeoutInSec) throws InterruptedException { - assertThat(msg, awaitBusy(new Predicate() { + boolean success = awaitBusy(new Predicate() { @Override public boolean apply(Object o) { for (TestPluginServiceBase pluginService : consumerPluginServices) { @@ -162,7 +162,9 @@ public abstract class AbstractLicensesIntegrationTests extends ElasticsearchInte } return true; } - }, timeoutInSec, TimeUnit.SECONDS), equalTo(true)); + }, timeoutInSec + 1, TimeUnit.SECONDS); + logger.debug("Notification assertion complete"); + assertThat(msg, success, equalTo(true)); } diff --git a/plugin/src/test/java/org/elasticsearch/license/plugin/AbstractLicensesServiceTests.java b/plugin/src/test/java/org/elasticsearch/license/plugin/AbstractLicensesServiceTests.java index 5ef0a17598d..74d294f0239 100644 --- a/plugin/src/test/java/org/elasticsearch/license/plugin/AbstractLicensesServiceTests.java +++ b/plugin/src/test/java/org/elasticsearch/license/plugin/AbstractLicensesServiceTests.java @@ -20,11 +20,10 @@ import org.elasticsearch.license.plugin.core.LicensesStatus; import org.elasticsearch.test.InternalTestCluster; import org.junit.Before; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.*; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import static org.elasticsearch.license.plugin.core.LicensesService.LicensesUpdateResponse; import static org.hamcrest.Matchers.equalTo; @@ -80,7 +79,22 @@ public abstract class AbstractLicensesServiceTests extends AbstractLicensesInteg return new Action(new Runnable() { @Override public void run() { - clientService.register(feature, new LicensesService.TrialLicenseOptions(expiryDuration, 10), + clientService.register(feature, new LicensesService.TrialLicenseOptions(expiryDuration, 10), Collections.emptyList(), + clientListener); + + // invoke clusterChanged event to flush out pendingRegistration + LicensesService licensesService = (LicensesService) clientService; + ClusterChangedEvent event = new ClusterChangedEvent("", clusterService().state(), clusterService().state()); + licensesService.clusterChanged(event); + } + }, 0, 1, "should trigger onEnable for " + feature + " once [trial license]"); + } + + protected Action registerWithEventNotification(final LicensesClientService clientService, final LicensesClientService.Listener clientListener, final String feature, final TimeValue expiryDuration, final Collection expirationCallbacks) { + return new Action(new Runnable() { + @Override + public void run() { + clientService.register(feature, new LicensesService.TrialLicenseOptions(expiryDuration, 10), expirationCallbacks, clientListener); // invoke clusterChanged event to flush out pendingRegistration @@ -94,15 +108,18 @@ public abstract class AbstractLicensesServiceTests extends AbstractLicensesInteg protected class TestTrackingClientListener implements LicensesClientService.Listener { CountDownLatch enableLatch; CountDownLatch disableLatch; + AtomicBoolean enabled = new AtomicBoolean(false); final boolean track; + final String featureName; - public TestTrackingClientListener(boolean track) { - this.track = track; + public TestTrackingClientListener(String featureName) { + this(featureName, true); } - public TestTrackingClientListener() { - this(true); + public TestTrackingClientListener(String featureName, boolean track) { + this.track = track; + this.featureName = featureName; } public synchronized void latch(CountDownLatch enableLatch, CountDownLatch disableLatch) { @@ -111,14 +128,20 @@ public abstract class AbstractLicensesServiceTests extends AbstractLicensesInteg } @Override - public void onEnabled() { + public void onEnabled(License license) { + assertNotNull(license); + assertThat(license.feature(), equalTo(featureName)); + enabled.set(true); if (track) { this.enableLatch.countDown(); } } @Override - public void onDisabled() { + public void onDisabled(License license) { + assertNotNull(license); + assertThat(license.feature(), equalTo(featureName)); + enabled.set(false); if (track) { this.disableLatch.countDown(); } diff --git a/plugin/src/test/java/org/elasticsearch/license/plugin/LicensesClientServiceTests.java b/plugin/src/test/java/org/elasticsearch/license/plugin/LicensesClientServiceTests.java index cfc9c6b51c5..f5f4f4d0166 100644 --- a/plugin/src/test/java/org/elasticsearch/license/plugin/LicensesClientServiceTests.java +++ b/plugin/src/test/java/org/elasticsearch/license/plugin/LicensesClientServiceTests.java @@ -22,7 +22,7 @@ import java.util.concurrent.atomic.AtomicLong; import static org.elasticsearch.license.plugin.TestUtils.generateSignedLicense; import static org.elasticsearch.test.ElasticsearchIntegrationTest.ClusterScope; import static org.elasticsearch.test.ElasticsearchIntegrationTest.Scope.TEST; -import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.*; @ClusterScope(scope = TEST, numDataNodes = 10) public class LicensesClientServiceTests extends AbstractLicensesServiceTests { @@ -32,11 +32,11 @@ public class LicensesClientServiceTests extends AbstractLicensesServiceTests { // register with trial license and assert onEnable and onDisable notification final LicensesClientService clientService = licensesClientService(); - final TestTrackingClientListener clientListener = new TestTrackingClientListener(); + String feature1 = "feature1"; + final TestTrackingClientListener clientListener = new TestTrackingClientListener(feature1); List actions = new ArrayList<>(); final TimeValue expiryDuration = TimeValue.timeValueSeconds(2); - String feature1 = "feature1"; actions.add(registerWithTrialLicense(clientService, clientListener, feature1, expiryDuration)); actions.add(assertExpiryAction(feature1, "trial", expiryDuration)); assertClientListenerNotificationCount(clientListener, actions); @@ -45,37 +45,212 @@ public class LicensesClientServiceTests extends AbstractLicensesServiceTests { @Test public void testLicenseWithFutureIssueDate() throws Exception { final LicensesClientService clientService = licensesClientService(); - final TestTrackingClientListener clientListener = new TestTrackingClientListener(); String feature = "feature"; + final TestTrackingClientListener clientListener = new TestTrackingClientListener(feature); List actions = new ArrayList<>(); long now = System.currentTimeMillis(); long issueDate = dateMath("now+10d/d", now); - actions.add(registerWithTrialLicense(clientService, clientListener, feature, TimeValue.timeValueSeconds(2))); + actions.add(registerWithoutTrialLicense(clientService, clientListener, feature)); actions.add(generateAndPutSignedLicenseAction(masterLicensesManagerService(), feature, issueDate, TimeValue.timeValueHours(24 * 20))); - actions.add(assertExpiryAction(feature, "trial", TimeValue.timeValueSeconds(2))); assertClientListenerNotificationCount(clientListener, actions); + assertThat(clientListener.enabled.get(), equalTo(false)); } + @Test + public void testPostExpiration() throws Exception { + int postExpirySeconds = randomIntBetween(5, 10); + TimeValue postExpiryDuration = TimeValue.timeValueSeconds(postExpirySeconds); + TimeValue min = TimeValue.timeValueSeconds(postExpirySeconds - randomIntBetween(1, 3)); + TimeValue max = TimeValue.timeValueSeconds(postExpirySeconds + randomIntBetween(1, 10)); + + final LicensesService.ExpirationCallback.Post post = new LicensesService.ExpirationCallback.Post(min, max, TimeValue.timeValueMillis(10)) { + + @Override + public void on(License license, LicensesService.ExpirationStatus status) { + } + }; + long now = System.currentTimeMillis(); + assertThat(post.matches(now - postExpiryDuration.millis(), now), equalTo(true)); + assertThat(post.matches(now + postExpiryDuration.getMillis(), now), equalTo(false)); + } + + @Test + public void testPostExpirationWithNullMax() throws Exception { + int postExpirySeconds = randomIntBetween(5, 10); + TimeValue postExpiryDuration = TimeValue.timeValueSeconds(postExpirySeconds); + TimeValue min = TimeValue.timeValueSeconds(postExpirySeconds - randomIntBetween(1, 3)); + + final LicensesService.ExpirationCallback.Post post = new LicensesService.ExpirationCallback.Post(min, null, TimeValue.timeValueMillis(10)) { + + @Override + public void on(License license, LicensesService.ExpirationStatus status) { + } + }; + long now = System.currentTimeMillis(); + assertThat(post.matches(now - postExpiryDuration.millis(), now), equalTo(true)); + } + + @Test + public void testPreExpirationWithNullMin() throws Exception { + int expirySeconds = randomIntBetween(5, 10); + TimeValue expiryDuration = TimeValue.timeValueSeconds(expirySeconds); + TimeValue max = TimeValue.timeValueSeconds(expirySeconds + randomIntBetween(1, 10)); + + final LicensesService.ExpirationCallback.Pre pre = new LicensesService.ExpirationCallback.Pre(null, max, TimeValue.timeValueMillis(10)) { + + @Override + public void on(License license, LicensesService.ExpirationStatus status) { + } + }; + long now = System.currentTimeMillis(); + assertThat(pre.matches(expiryDuration.millis() + now, now), equalTo(true)); + } + + @Test + public void testPreExpiration() throws Exception { + int expirySeconds = randomIntBetween(5, 10); + TimeValue expiryDuration = TimeValue.timeValueSeconds(expirySeconds); + TimeValue min = TimeValue.timeValueSeconds(expirySeconds - randomIntBetween(0, 3)); + TimeValue max = TimeValue.timeValueSeconds(expirySeconds + randomIntBetween(1, 10)); + + final LicensesService.ExpirationCallback.Pre pre = new LicensesService.ExpirationCallback.Pre(min, max, TimeValue.timeValueMillis(10)) { + + @Override + public void on(License license, LicensesService.ExpirationStatus status) { + } + }; + long now = System.currentTimeMillis(); + assertThat(pre.matches(expiryDuration.millis() + now, now), equalTo(true)); + assertThat(pre.matches(now - expiryDuration.getMillis(), now), equalTo(false)); + } + + @Test + public void testMultipleEventNotification() throws Exception { + final LicensesManagerService licensesManagerService = masterLicensesManagerService(); + final LicensesClientService clientService = licensesClientService(); + final String feature = "feature"; + TestTrackingClientListener clientListener = new TestTrackingClientListener(feature, true); + + List callbacks = new ArrayList<>(); + final AtomicInteger triggerCount1 = new AtomicInteger(0); + callbacks.add(preCallbackLatch(TimeValue.timeValueMillis(500), TimeValue.timeValueSeconds(1), TimeValue.timeValueMillis(100), triggerCount1)); + final AtomicInteger triggerCount2 = new AtomicInteger(0); + callbacks.add(postCallbackLatch(TimeValue.timeValueMillis(0), null, TimeValue.timeValueMillis(200), triggerCount2)); + final AtomicInteger triggerCount3 = new AtomicInteger(0); + callbacks.add(preCallbackLatch(TimeValue.timeValueSeconds(1), TimeValue.timeValueSeconds(2), TimeValue.timeValueMillis(500), triggerCount3)); + + List actions = new ArrayList<>(); + actions.add(registerWithEventNotification(clientService, clientListener, feature, TimeValue.timeValueSeconds(3), callbacks)); + actions.add(assertExpiryAction(feature, "trial", TimeValue.timeValueMinutes(1))); + assertClientListenerNotificationCount(clientListener, actions); + assertThat(triggerCount3.get(), equalTo(2)); + assertThat(triggerCount1.get(), greaterThan(4)); + Thread.sleep(2000); + assertThat(triggerCount2.get(), greaterThan(8)); + int previousTriggerCount = triggerCount2.get(); + + // Update license + generateAndPutSignedLicenseAction(licensesManagerService, feature, TimeValue.timeValueSeconds(10)).run(); + Thread.sleep(500); + assertThat(previousTriggerCount, lessThanOrEqualTo(triggerCount2.get() + 1)); + } + + + @Test + public void testPostEventNotification2() throws Exception { + final LicensesClientService clientService = licensesClientService(); + final String feature = "feature"; + TestTrackingClientListener clientListener = new TestTrackingClientListener(feature, true); + AtomicInteger counter = new AtomicInteger(0); + List actions = new ArrayList<>(); + actions.add( + registerWithEventNotification(clientService, clientListener, feature, TimeValue.timeValueSeconds(3), + Arrays.asList( + postCallbackLatch(TimeValue.timeValueMillis(0), TimeValue.timeValueSeconds(2), TimeValue.timeValueMillis(500), counter) + )) + ); + actions.add(assertExpiryAction(feature, "trial", TimeValue.timeValueSeconds(3))); + assertClientListenerNotificationCount(clientListener, actions); + Thread.sleep(50 + 2000); + assertThat(counter.get(), equalTo(4)); + } + + @Test + public void testPreEventNotification() throws Exception { + final LicensesClientService clientService = licensesClientService(); + final String feature = "feature"; + TestTrackingClientListener clientListener = new TestTrackingClientListener(feature, true); + AtomicInteger counter = new AtomicInteger(0); + List actions = new ArrayList<>(); + actions.add( + registerWithEventNotification(clientService, clientListener, feature, TimeValue.timeValueSeconds(3), + Arrays.asList( + preCallbackLatch(TimeValue.timeValueMillis(500), TimeValue.timeValueSeconds(2), TimeValue.timeValueMillis(500), counter) + )) + ); + actions.add(assertExpiryAction(feature, "trial", TimeValue.timeValueSeconds(3))); + assertClientListenerNotificationCount(clientListener, actions); + assertThat(counter.get(), equalTo(3)); + } + + @Test + public void testPostEventNotification() throws Exception { + final LicensesClientService clientService = licensesClientService(); + final String feature = "feature"; + TestTrackingClientListener clientListener = new TestTrackingClientListener(feature, true); + AtomicInteger counter = new AtomicInteger(0); + List actions = new ArrayList<>(); + actions.add( + registerWithEventNotification(clientService, clientListener, feature, TimeValue.timeValueSeconds(1), + Arrays.asList( + postCallbackLatch(TimeValue.timeValueMillis(500), TimeValue.timeValueSeconds(2), TimeValue.timeValueMillis(500), counter) + )) + ); + actions.add(assertExpiryAction(feature, "trial", TimeValue.timeValueSeconds(1))); + assertClientListenerNotificationCount(clientListener, actions); + Thread.sleep(50 + 2000); + assertThat(counter.get(), equalTo(3)); + } + + private LicensesService.ExpirationCallback preCallbackLatch(TimeValue min, TimeValue max, TimeValue frequency, final AtomicInteger triggerCount) { + return new LicensesService.ExpirationCallback.Pre(min, max, frequency) { + @Override + public void on(License license, LicensesService.ExpirationStatus status) { + triggerCount.incrementAndGet(); + } + }; + } + + private LicensesService.ExpirationCallback postCallbackLatch(TimeValue min, TimeValue max, TimeValue frequency, final AtomicInteger triggerCount) { + return new LicensesService.ExpirationCallback.Post(min, max, frequency) { + @Override + public void on(License license, LicensesService.ExpirationStatus status) { + triggerCount.incrementAndGet(); + } + }; + } + + @Test public void testMultipleClientSignedLicenseEnforcement() throws Exception { // multiple client registration with null trial license and then different expiry signed license final LicensesManagerService masterLicensesManagerService = masterLicensesManagerService(); final LicensesService licensesService = randomLicensesService(); - final TestTrackingClientListener clientListener1 = new TestTrackingClientListener(); - final TestTrackingClientListener clientListener2 = new TestTrackingClientListener(); + String feature1 = "feature1"; + String feature2 = "feature2"; + final TestTrackingClientListener clientListener1 = new TestTrackingClientListener(feature1); + final TestTrackingClientListener clientListener2 = new TestTrackingClientListener(feature2); List firstClientActions = new ArrayList<>(); List secondClientActions = new ArrayList<>(); final TimeValue firstExpiryDuration = TimeValue.timeValueSeconds(2); - String feature1 = "feature1"; firstClientActions.add(registerWithoutTrialLicense(licensesService, clientListener1, feature1)); firstClientActions.add(generateAndPutSignedLicenseAction(masterLicensesManagerService, feature1, firstExpiryDuration)); firstClientActions.add(assertExpiryAction(feature1, "signed", firstExpiryDuration)); final TimeValue secondExpiryDuration = TimeValue.timeValueSeconds(1); - String feature2 = "feature2"; secondClientActions.add(registerWithoutTrialLicense(licensesService, clientListener2, feature2)); secondClientActions.add(generateAndPutSignedLicenseAction(masterLicensesManagerService, feature2, secondExpiryDuration)); secondClientActions.add(assertExpiryAction(feature2, "signed", secondExpiryDuration)); @@ -95,19 +270,19 @@ public class LicensesClientServiceTests extends AbstractLicensesServiceTests { final LicensesManagerService masterLicensesManagerService = masterLicensesManagerService(); final LicensesService licensesService = randomLicensesService(); - final TestTrackingClientListener clientListener1 = new TestTrackingClientListener(); - final TestTrackingClientListener clientListener2 = new TestTrackingClientListener(); + String feature1 = "feature1"; + String feature2 = "feature2"; + final TestTrackingClientListener clientListener1 = new TestTrackingClientListener(feature1); + final TestTrackingClientListener clientListener2 = new TestTrackingClientListener(feature2); List firstClientActions = new ArrayList<>(); List secondClientActions = new ArrayList<>(); final TimeValue firstExpiryDuration = TimeValue.timeValueSeconds(2); - String feature1 = "feature1"; firstClientActions.add(registerWithoutTrialLicense(licensesService, clientListener1, feature1)); firstClientActions.add(generateAndPutSignedLicenseAction(masterLicensesManagerService, feature1, firstExpiryDuration)); firstClientActions.add(assertExpiryAction(feature1, "signed", firstExpiryDuration)); final TimeValue secondExpiryDuration = TimeValue.timeValueSeconds(1); - String feature2 = "feature2"; secondClientActions.add(registerWithTrialLicense(licensesService, clientListener2, feature2, secondExpiryDuration)); secondClientActions.add(assertExpiryAction(feature2, "trial", secondExpiryDuration)); @@ -125,18 +300,18 @@ public class LicensesClientServiceTests extends AbstractLicensesServiceTests { // multiple client registration: both with trail license of different expiryDuration final LicensesService licensesService = randomLicensesService(); - final TestTrackingClientListener clientListener1 = new TestTrackingClientListener(); - final TestTrackingClientListener clientListener2 = new TestTrackingClientListener(); + String feature1 = "feature1"; + String feature2 = "feature2"; + final TestTrackingClientListener clientListener1 = new TestTrackingClientListener(feature1); + final TestTrackingClientListener clientListener2 = new TestTrackingClientListener(feature2); List firstClientActions = new ArrayList<>(); List secondClientActions = new ArrayList<>(); TimeValue firstExpiryDuration = TimeValue.timeValueSeconds(1); - String feature1 = "feature1"; firstClientActions.add(registerWithTrialLicense(licensesService, clientListener1, feature1, firstExpiryDuration)); firstClientActions.add(assertExpiryAction(feature1, "trial", firstExpiryDuration)); TimeValue secondExpiryDuration = TimeValue.timeValueSeconds(2); - String feature2 = "feature2"; secondClientActions.add(registerWithTrialLicense(licensesService, clientListener2, feature2, secondExpiryDuration)); secondClientActions.add(assertExpiryAction(feature2, "trial", secondExpiryDuration)); @@ -153,21 +328,22 @@ public class LicensesClientServiceTests extends AbstractLicensesServiceTests { public void testFeatureWithoutLicense() throws Exception { // client registration with no trial license + no signed license final LicensesClientService clientService = licensesClientService(); - final TestTrackingClientListener clientListener = new TestTrackingClientListener(); + String feature = "feature1"; + final TestTrackingClientListener clientListener = new TestTrackingClientListener(feature); List actions = new ArrayList<>(); - actions.add(registerWithoutTrialLicense(clientService, clientListener, "feature1")); + actions.add(registerWithoutTrialLicense(clientService, clientListener, feature)); assertClientListenerNotificationCount(clientListener, actions); } @Test public void testLicenseExpiry() throws Exception { final LicensesClientService clientService = licensesClientService(); - final TestTrackingClientListener clientListener = new TestTrackingClientListener(); + String feature = "feature1"; + final TestTrackingClientListener clientListener = new TestTrackingClientListener(feature); List actions = new ArrayList<>(); TimeValue expiryDuration = TimeValue.timeValueSeconds(2); - String feature = "feature1"; actions.add(registerWithTrialLicense(clientService, clientListener, feature, expiryDuration)); actions.add(assertExpiryAction(feature, "trial", expiryDuration)); @@ -182,8 +358,8 @@ public class LicensesClientServiceTests extends AbstractLicensesServiceTests { TimeValue expiryDuration = TimeValue.timeValueSeconds(0); for (int i = 0; i < randomIntBetween(5, 10); i++) { - final TestTrackingClientListener clientListener = new TestTrackingClientListener(); String feature = "feature_" + String.valueOf(i); + final TestTrackingClientListener clientListener = new TestTrackingClientListener(feature); expiryDuration = TimeValue.timeValueMillis(randomIntBetween(1, 3) * 1000l + expiryDuration.millis()); List actions = new ArrayList<>(); @@ -227,7 +403,7 @@ public class LicensesClientServiceTests extends AbstractLicensesServiceTests { return new Action(new Runnable() { @Override public void run() { - clientService.register(feature, null, clientListener); + clientService.register(feature, null, Collections.emptyList(), clientListener); } }, 0, 0, "should not trigger any notification [disabled by default]"); } diff --git a/plugin/src/test/java/org/elasticsearch/license/plugin/LicensesManagerServiceTests.java b/plugin/src/test/java/org/elasticsearch/license/plugin/LicensesManagerServiceTests.java index bdbcdfab20f..ab268cb7b69 100644 --- a/plugin/src/test/java/org/elasticsearch/license/plugin/LicensesManagerServiceTests.java +++ b/plugin/src/test/java/org/elasticsearch/license/plugin/LicensesManagerServiceTests.java @@ -78,7 +78,7 @@ public class LicensesManagerServiceTests extends AbstractLicensesServiceTests { // generate a trial license for one feature final LicensesClientService clientService = licensesClientService(); - final TestTrackingClientListener clientListener = new TestTrackingClientListener(false); + final TestTrackingClientListener clientListener = new TestTrackingClientListener("shield", false); registerWithTrialLicense(clientService, clientListener, "shield", TimeValue.timeValueHours(1)).run(); // generate signed licenses for multiple features diff --git a/plugin/src/test/java/org/elasticsearch/license/plugin/LicensesPluginIntegrationTests.java b/plugin/src/test/java/org/elasticsearch/license/plugin/LicensesPluginIntegrationTests.java index 7d74626ad7b..ecc0ece2750 100644 --- a/plugin/src/test/java/org/elasticsearch/license/plugin/LicensesPluginIntegrationTests.java +++ b/plugin/src/test/java/org/elasticsearch/license/plugin/LicensesPluginIntegrationTests.java @@ -90,7 +90,7 @@ public class LicensesPluginIntegrationTests extends AbstractLicensesIntegrationT logger.info(" --> check trial license expiry notification"); // consumer plugin should notify onDisabled on all data nodes (expired signed license) - assertConsumerPluginDisabledNotification(trialLicenseDurationInSeconds); + assertConsumerPluginDisabledNotification(trialLicenseDurationInSeconds * 2); assertLicenseManagerDisabledFeatureFor(getCurrentFeatureName()); } diff --git a/plugin/src/test/java/org/elasticsearch/license/plugin/LicensesTransportTests.java b/plugin/src/test/java/org/elasticsearch/license/plugin/LicensesTransportTests.java index a41efd2e8c8..3f8412addb2 100644 --- a/plugin/src/test/java/org/elasticsearch/license/plugin/LicensesTransportTests.java +++ b/plugin/src/test/java/org/elasticsearch/license/plugin/LicensesTransportTests.java @@ -111,7 +111,7 @@ public class LicensesTransportTests extends AbstractLicensesIntegrationTests { @Test public void testPutExpiredLicense() throws Exception { - License expiredLicense = generateSignedLicense("expiredFeature", dateMath("now-10d/d", System.currentTimeMillis()), TimeValue.timeValueMinutes(2)); + License expiredLicense = generateSignedLicense("feature", dateMath("now-10d/d", System.currentTimeMillis()), TimeValue.timeValueMinutes(2)); License signedLicense = generateSignedLicense("feature", TimeValue.timeValueMinutes(2)); PutLicenseRequestBuilder builder = new PutLicenseRequestBuilder(client().admin().cluster()); diff --git a/plugin/src/test/java/org/elasticsearch/license/plugin/consumer/TestPluginServiceBase.java b/plugin/src/test/java/org/elasticsearch/license/plugin/consumer/TestPluginServiceBase.java index f7586ca06de..228212aa1b5 100644 --- a/plugin/src/test/java/org/elasticsearch/license/plugin/consumer/TestPluginServiceBase.java +++ b/plugin/src/test/java/org/elasticsearch/license/plugin/consumer/TestPluginServiceBase.java @@ -13,9 +13,13 @@ import org.elasticsearch.common.component.AbstractLifecycleComponent; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.gateway.GatewayService; +import org.elasticsearch.license.core.License; import org.elasticsearch.license.plugin.core.LicensesClientService; import org.elasticsearch.license.plugin.core.LicensesService; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.concurrent.atomic.AtomicBoolean; public abstract class TestPluginServiceBase extends AbstractLifecycleComponent implements ClusterStateListener { @@ -29,6 +33,37 @@ public abstract class TestPluginServiceBase extends AbstractLifecycleComponent expirationCallbacks; + + static { + // Callback triggered every 24 hours from 30 days to 7 days of license expiry + final LicensesService.ExpirationCallback.Pre LEVEL_1 = new LicensesService.ExpirationCallback.Pre(TimeValue.timeValueHours(7 * 24), TimeValue.timeValueHours(30 * 24), TimeValue.timeValueHours(24)) { + @Override + public void on(License license, LicensesService.ExpirationStatus status) { + } + }; + + // Callback triggered every 10 minutes from 7 days to license expiry + final LicensesService.ExpirationCallback.Pre LEVEL_2 = new LicensesService.ExpirationCallback.Pre(null, TimeValue.timeValueHours(7 * 24), TimeValue.timeValueMinutes(10)) { + @Override + public void on(License license, LicensesService.ExpirationStatus status) { + } + }; + + // Callback triggered every 10 minutes after license expiry + final LicensesService.ExpirationCallback.Post LEVEL_3 = new LicensesService.ExpirationCallback.Post(TimeValue.timeValueMillis(0), null, TimeValue.timeValueMinutes(10)) { + @Override + public void on(License license, LicensesService.ExpirationStatus status) { + } + }; + + expirationCallbacks = new ArrayList<>(); + expirationCallbacks.add(LEVEL_1); + expirationCallbacks.add(LEVEL_2); + expirationCallbacks.add(LEVEL_3); + } + + public final AtomicBoolean registered = new AtomicBoolean(false); private volatile AtomicBoolean enabled = new AtomicBoolean(false); @@ -68,7 +103,7 @@ public abstract class TestPluginServiceBase extends AbstractLifecycleComponentorg.elasticsearch elasticsearch-license pom - 1.0.0-beta2 + 1.0.0-SNAPSHOT core-shaded core