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@11b53dd78d
This commit is contained in:
Areek Zillur 2015-01-21 14:40:09 -05:00
parent 26fa372056
commit 5be5b1915b
15 changed files with 541 additions and 90 deletions

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>elasticsearch-license</artifactId>
<groupId>org.elasticsearch</groupId>
<version>1.0.0-beta2</version>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>elasticsearch-license</artifactId>
<groupId>org.elasticsearch</groupId>
<version>1.0.0-beta2</version>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<properties>
@ -17,7 +17,7 @@
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch-license-core-shaded</artifactId>
<version>1.0.0-beta2</version>
<version>1.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
<build>

View File

@ -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);
}
}

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>elasticsearch-license</artifactId>
<groupId>org.elasticsearch</groupId>
<version>1.0.0-beta2</version>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@ -19,7 +19,7 @@
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch-license-core</artifactId>
<version>1.0.0-beta2</version>
<version>1.0.0-SNAPSHOT</version>
</dependency>
</dependencies>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>elasticsearch-license</artifactId>
<groupId>org.elasticsearch</groupId>
<version>1.0.0-beta2</version>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@ -21,13 +21,13 @@
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch-license-licensor</artifactId>
<version>1.0.0-beta2</version>
<version>1.0.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch-license-core</artifactId>
<version>1.0.0-beta2</version>
<version>1.0.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>

View File

@ -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 <code>null</code> 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<ExpirationCallback> expirationCallbacks, Listener listener);
}

View File

@ -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;
* <p/>
* Registration Scheme:
* <p/>
* 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<org.elasticsearch.license.plugin.core.LicensesService.ExpirationCallback>, 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<LicensesService>
*/
private final Queue<ScheduledFuture> scheduledNotifications = new ConcurrentLinkedQueue<>();
/**
* Currently active event notifications for every registered feature
*/
private final Map<String, Queue<ScheduledFuture>> eventNotificationsMap = new HashMap<>();
/**
* The last licensesMetaData that has been notified by {@link #notifyFeatures(LicensesMetaData)}
*/
@ -352,9 +355,18 @@ public class LicensesService extends AbstractLifecycleComponent<LicensesService>
scheduledNotification.cancel(true);
}
for (Queue<ScheduledFuture> queue : eventNotificationsMap.values()) {
for (ScheduledFuture scheduledFuture : queue) {
scheduledFuture.cancel(true);
}
queue.clear();
}
LicensesMetaData currentLicensesMetaData = clusterService.state().metaData().custom(LicensesMetaData.TYPE);
final Map<String, License> 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<LicensesService>
} else {
notifyFeaturesAndScheduleNotificationIfNeeded(currentLicensesMetaData);
}
final Map<String, License> 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<LicensesService>
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<String, License> 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<LicensesService>
* {@inheritDoc}
*/
@Override
public void register(String feature, TrialLicenseOptions trialLicenseOptions, Listener listener) {
public void register(String feature, TrialLicenseOptions trialLicenseOptions, Collection<ExpirationCallback> 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<ScheduledFuture> 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<LicensesService>
} 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<LicensesService>
// 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<String, License> effectiveLicenses = getEffectiveLicenses(currentMetaData);
listenerHolder.scheduleNotificationIfNeeded(effectiveLicenses.get(listenerHolder.feature));
return true;
}
private long issueDateForFeature(String feature, final LicensesMetaData currentLicensesMetaData) {
final Map<String, License> 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<String, License> effectiveLicenses = getEffectiveLicenses(currentLicensesMetaData);
License featureLicense;
if ((featureLicense = effectiveLicenses.get(feature)) != null) {
@ -617,7 +638,7 @@ public class LicensesService extends AbstractLifecycleComponent<LicensesService>
return -1l;
}
private Map<String, License> getEffectiveLicenses(final LicensesMetaData metaData) {
private static Map<String, License> getEffectiveLicenses(final LicensesMetaData metaData) {
Map<String, License> map = new HashMap<>();
if (metaData != null) {
Set<License> licenses = new HashSet<>();
@ -634,10 +655,9 @@ public class LicensesService extends AbstractLifecycleComponent<LicensesService>
}
/**
* Clears out any completed notification future from
* {@link #scheduledNotifications}
* Clears out any completed notification futures
*/
private void clearFinishedNotifications() {
private static void clearFinishedNotifications(Queue<ScheduledFuture> scheduledNotifications) {
while (!scheduledNotifications.isEmpty()) {
ScheduledFuture notification = scheduledNotifications.peek();
if (notification != null && notification.isDone()) {
@ -658,7 +678,7 @@ public class LicensesService extends AbstractLifecycleComponent<LicensesService>
* Schedules an expiry notification with a delay of <code>nextScheduleDelay</code>
*/
private void scheduleNextNotification(long nextScheduleDelay) {
clearFinishedNotifications();
clearFinishedNotifications(scheduledNotifications);
try {
final TimeValue delay = TimeValue.timeValueMillis(nextScheduleDelay);
@ -697,7 +717,6 @@ public class LicensesService extends AbstractLifecycleComponent<LicensesService>
}
}
public static class PutLicenseRequestHolder {
private final PutLicenseRequest request;
private final String source;
@ -728,38 +747,223 @@ public class LicensesService extends AbstractLifecycleComponent<LicensesService>
}
}
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<ExpirationCallback> expirationCallbacks;
final Listener listener;
final AtomicLong currentExpiryDate = new AtomicLong(-1l);
final Queue<ScheduledFuture> 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<ExpirationCallback> expirationCallbacks, Listener listener, Queue<ScheduledFuture> 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<String, License> 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() + ")";
}

View File

@ -152,7 +152,7 @@ public abstract class AbstractLicensesIntegrationTests extends ElasticsearchInte
}
private void assertConsumerPluginNotification(String msg, final Iterable<TestPluginServiceBase> consumerPluginServices, final boolean expectedEnabled, int timeoutInSec) throws InterruptedException {
assertThat(msg, awaitBusy(new Predicate<Object>() {
boolean success = awaitBusy(new Predicate<Object>() {
@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));
}

View File

@ -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.<LicensesService.ExpirationCallback>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<LicensesService.ExpirationCallback> 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();
}

View File

@ -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<Action> 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<Action> 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<LicensesService.ExpirationCallback> 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<Action> 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<Action> 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<Action> 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<Action> 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<Action> firstClientActions = new ArrayList<>();
List<Action> 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<Action> firstClientActions = new ArrayList<>();
List<Action> 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<Action> firstClientActions = new ArrayList<>();
List<Action> 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<Action> 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<Action> 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<Action> 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.<LicensesService.ExpirationCallback>emptyList(), clientListener);
}
}, 0, 0, "should not trigger any notification [disabled by default]");
}

View File

@ -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

View File

@ -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());
}

View File

@ -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());

View File

@ -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<TestPluginServiceBase> implements ClusterStateListener {
@ -29,6 +33,37 @@ public abstract class TestPluginServiceBase extends AbstractLifecycleComponent<T
final boolean eagerLicenseRegistration;
final static Collection<LicensesService.ExpirationCallback> 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 AbstractLifecycleComponent<T
if (registered.compareAndSet(false, true)) {
logger.info("Registering to licensesService [lazy]");
licensesClientService.register(featureName(),
trialLicenseOptions,
trialLicenseOptions, expirationCallbacks,
new LicensingClientListener());
}
}
@ -79,7 +114,7 @@ public abstract class TestPluginServiceBase extends AbstractLifecycleComponent<T
if (registered.compareAndSet(false, true)) {
logger.info("Registering to licensesService [eager]");
licensesClientService.register(featureName(),
trialLicenseOptions,
trialLicenseOptions, expirationCallbacks,
new LicensingClientListener());
}
}
@ -99,13 +134,18 @@ public abstract class TestPluginServiceBase extends AbstractLifecycleComponent<T
private class LicensingClientListener implements LicensesClientService.Listener {
@Override
public void onEnabled() {
public void onEnabled(License license) {
logger.info("TestConsumerPlugin: " + license.feature() + " enabled");
enabled.set(true);
}
@Override
public void onDisabled() {
public void onDisabled(License license) {
if (license != null) {
logger.info("TestConsumerPlugin: " + license.feature() + " disabled");
}
enabled.set(false);
}
}
}

View File

@ -7,7 +7,7 @@
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch-license</artifactId>
<packaging>pom</packaging>
<version>1.0.0-beta2</version>
<version>1.0.0-SNAPSHOT</version>
<modules>
<module>core-shaded</module>
<module>core</module>