[fix] Removing license did not update the Licensees

- Introduced a `MISSING` operation mode
- now when the license is removed (and a tombstone license is placed), the licensees get notified with a `MISSING` license status
- the monitoring, security and watcher licensees were updated

Original commit: elastic/x-pack-elasticsearch@650d940666
This commit is contained in:
uboness 2016-04-22 14:35:46 -07:00 committed by Areek Zillur
parent 233c64e942
commit 06a0a9cbb5
10 changed files with 191 additions and 107 deletions

View File

@ -77,6 +77,7 @@ public class License implements ToXContent {
* Note: The mode indicates features that should be made available, but it does not indicate whether the license is active!
*/
public enum OperationMode {
MISSING,
TRIAL,
BASIC,
STANDARD,
@ -85,6 +86,8 @@ public class License implements ToXContent {
public static OperationMode resolve(String type) {
switch (type.toLowerCase(Locale.ROOT)) {
case "missing":
return MISSING;
case "trial":
case "none": // bwc for 1.x subscription_type field
case "dev": // bwc for 1.x subscription_type field

View File

@ -53,6 +53,7 @@ public interface Licensee {
class Status {
public static Status ENABLED = new Status(OperationMode.TRIAL, LicenseState.ENABLED);
public static Status MISSING = new Status(OperationMode.MISSING, LicenseState.DISABLED);
private final OperationMode mode;
private final LicenseState licenseState;

View File

@ -511,72 +511,77 @@ public class LicensesService extends AbstractLifecycleComponent<LicensesService>
*/
private void notifyAndSchedule(final LicensesMetaData currentLicensesMetaData) {
final License license = getLicense(currentLicensesMetaData);
if (license != null) {
logger.debug("notifying [{}] listeners", registeredLicensees.size());
long now = System.currentTimeMillis();
if (license.issueDate() > now) {
logger.info("license [{}] - invalid", license.uid());
return;
if (license == null) {
for (InternalLicensee licensee : registeredLicensees) {
licensee.onRemove();
}
long expiryDuration = license.expiryDate() - now;
if (license.expiryDate() > now) {
for (InternalLicensee licensee : registeredLicensees) {
licensee.onChange(license, LicenseState.ENABLED);
}
logger.info("license [{}] - valid", license.uid());
final TimeValue delay = TimeValue.timeValueMillis(expiryDuration);
// cancel any previous notifications
cancelNotifications(expiryNotifications);
try {
logger.debug("schedule grace notification after [{}] for license [{}]", delay.toString(), license.uid());
expiryNotifications.add(threadPool.schedule(delay, executorName(), new LicensingClientNotificationJob()));
} catch (EsRejectedExecutionException ex) {
logger.debug("couldn't schedule grace notification", ex);
}
} else if ((license.expiryDate() + gracePeriodDuration.getMillis()) > now) {
for (InternalLicensee licensee : registeredLicensees) {
licensee.onChange(license, LicenseState.GRACE_PERIOD);
}
logger.info("license [{}] - grace", license.uid());
final TimeValue delay = TimeValue.timeValueMillis(expiryDuration + gracePeriodDuration.getMillis());
// cancel any previous notifications
cancelNotifications(expiryNotifications);
try {
logger.debug("schedule expiry notification after [{}] for license [{}]", delay.toString(), license.uid());
expiryNotifications.add(threadPool.schedule(delay, executorName(), new LicensingClientNotificationJob()));
} catch (EsRejectedExecutionException ex) {
logger.debug("couldn't schedule expiry notification", ex);
}
} else {
for (InternalLicensee licensee : registeredLicensees) {
licensee.onChange(license, LicenseState.DISABLED);
}
logger.info("license [{}] - expired", license.uid());
return;
}
logger.debug("notifying [{}] listeners", registeredLicensees.size());
long now = System.currentTimeMillis();
if (license.issueDate() > now) {
logger.info("license [{}] - invalid", license.uid());
return;
}
long expiryDuration = license.expiryDate() - now;
if (license.expiryDate() > now) {
for (InternalLicensee licensee : registeredLicensees) {
licensee.onChange(license, LicenseState.ENABLED);
}
if (!license.equals(currentLicense.get())) {
currentLicense.set(license);
// cancel all scheduled event notifications
cancelNotifications(eventNotifications);
// schedule expiry callbacks
for (ExpirationCallback expirationCallback : this.expirationCallbacks) {
final TimeValue delay;
if (expirationCallback.matches(license.expiryDate(), now)) {
expirationCallback.on(license);
TimeValue frequency = expirationCallback.frequency();
delay = frequency != null ? frequency : expirationCallback.delay(expiryDuration);
} else {
delay = expirationCallback.delay(expiryDuration);
}
if (delay != null) {
eventNotifications.add(threadPool.schedule(delay, executorName(), new EventNotificationJob(expirationCallback)));
}
if (logger.isDebugEnabled()) {
logger.debug("schedule [{}] after [{}]", expirationCallback, delay);
}
}
logger.debug("scheduled expiry callbacks for [{}] expiring after [{}]", license.uid(),
TimeValue.timeValueMillis(expiryDuration));
logger.info("license [{}] - valid", license.uid());
final TimeValue delay = TimeValue.timeValueMillis(expiryDuration);
// cancel any previous notifications
cancelNotifications(expiryNotifications);
try {
logger.debug("schedule grace notification after [{}] for license [{}]", delay.toString(), license.uid());
expiryNotifications.add(threadPool.schedule(delay, executorName(), new LicensingClientNotificationJob()));
} catch (EsRejectedExecutionException ex) {
logger.debug("couldn't schedule grace notification", ex);
}
} else if ((license.expiryDate() + gracePeriodDuration.getMillis()) > now) {
for (InternalLicensee licensee : registeredLicensees) {
licensee.onChange(license, LicenseState.GRACE_PERIOD);
}
logger.info("license [{}] - grace", license.uid());
final TimeValue delay = TimeValue.timeValueMillis(expiryDuration + gracePeriodDuration.getMillis());
// cancel any previous notifications
cancelNotifications(expiryNotifications);
try {
logger.debug("schedule expiry notification after [{}] for license [{}]", delay.toString(), license.uid());
expiryNotifications.add(threadPool.schedule(delay, executorName(), new LicensingClientNotificationJob()));
} catch (EsRejectedExecutionException ex) {
logger.debug("couldn't schedule expiry notification", ex);
}
} else {
for (InternalLicensee licensee : registeredLicensees) {
licensee.onChange(license, LicenseState.DISABLED);
}
logger.info("license [{}] - expired", license.uid());
}
if (!license.equals(currentLicense.get())) {
currentLicense.set(license);
// cancel all scheduled event notifications
cancelNotifications(eventNotifications);
// schedule expiry callbacks
for (ExpirationCallback expirationCallback : this.expirationCallbacks) {
final TimeValue delay;
if (expirationCallback.matches(license.expiryDate(), now)) {
expirationCallback.on(license);
TimeValue frequency = expirationCallback.frequency();
delay = frequency != null ? frequency : expirationCallback.delay(expiryDuration);
} else {
delay = expirationCallback.delay(expiryDuration);
}
if (delay != null) {
eventNotifications.add(threadPool.schedule(delay, executorName(), new EventNotificationJob(expirationCallback)));
}
if (logger.isDebugEnabled()) {
logger.debug("schedule [{}] after [{}]", expirationCallback, delay);
}
}
logger.debug("scheduled expiry callbacks for [{}] expiring after [{}]", license.uid(),
TimeValue.timeValueMillis(expiryDuration));
}
}
@ -845,6 +850,14 @@ public class LicensesService extends AbstractLifecycleComponent<LicensesService>
}
}
}
public void onRemove() {
synchronized (this) {
currentLicense = null;
currentLicenseState = null;
licensee.onChange(Licensee.Status.MISSING);
}
}
}
/**

View File

@ -23,7 +23,6 @@ import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.license.core.License;
import org.elasticsearch.license.licensor.LicenseSigner;
import org.elasticsearch.license.plugin.action.put.PutLicenseRequest;
import org.elasticsearch.license.plugin.core.LicenseState;
import org.elasticsearch.license.plugin.core.Licensee;
import org.elasticsearch.license.plugin.core.LicensesService;
import org.elasticsearch.license.plugin.core.LicensesStatus;
@ -162,7 +161,7 @@ public class TestUtils {
public static class AssertingLicensee implements Licensee {
public final ESLogger logger;
public final String id;
public final List<LicenseState> licenseStates = new CopyOnWriteArrayList<>();
public final List<Licensee.Status> statuses = new CopyOnWriteArrayList<>();
public final AtomicInteger expirationMessagesCalled = new AtomicInteger(0);
public final List<Tuple<License, License>> acknowledgementRequested = new CopyOnWriteArrayList<>();
@ -196,7 +195,7 @@ public class TestUtils {
@Override
public void onChange(Status status) {
assertNotNull(status);
licenseStates.add(status.getLicenseState());
statuses.add(status);
}
}
}

View File

@ -11,7 +11,9 @@ import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.license.plugin.TestUtils.AssertingLicensee;
import org.elasticsearch.test.ESSingleNodeTestCase;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import static org.elasticsearch.license.plugin.TestUtils.awaitNoBlock;
import static org.elasticsearch.license.plugin.TestUtils.awaitNoPendingTasks;
@ -41,10 +43,10 @@ public class LicensesExpiryNotificationTests extends ESSingleNodeTestCase {
awaitNoBlock(client());
licensesService.register(licensee);
awaitNoPendingTasks(client());
boolean success = awaitBusy(() -> licensee.licenseStates.size() == 3);
boolean success = awaitBusy(() -> licensee.statuses.size() == 3);
// trail license: enable, grace, disabled
assertLicenseStates(licensee, LicenseState.ENABLED, LicenseState.GRACE_PERIOD, LicenseState.DISABLED);
assertTrue(dumpLicensingStates(licensee.licenseStates), success);
assertTrue(dumpLicensingStates(licensee.statuses), success);
licensesService.stop();
}
@ -61,10 +63,10 @@ public class LicensesExpiryNotificationTests extends ESSingleNodeTestCase {
licensesService.register(licensee1);
licensesService.register(licensee2);
awaitNoPendingTasks(client());
boolean success = awaitBusy(() -> licensee1.licenseStates.size() == 3);
assertTrue(dumpLicensingStates(licensee1.licenseStates), success);
success = awaitBusy(() -> licensee2.licenseStates.size() == 3);
assertTrue(dumpLicensingStates(licensee2.licenseStates), success);
boolean success = awaitBusy(() -> licensee1.statuses.size() == 3);
assertTrue(dumpLicensingStates(licensee1.statuses), success);
success = awaitBusy(() -> licensee2.statuses.size() == 3);
assertTrue(dumpLicensingStates(licensee2.statuses), success);
// trail license: enable, grace, disabled
assertLicenseStates(licensee1, LicenseState.ENABLED, LicenseState.GRACE_PERIOD, LicenseState.DISABLED);
assertLicenseStates(licensee2, LicenseState.ENABLED, LicenseState.GRACE_PERIOD, LicenseState.DISABLED);
@ -81,13 +83,13 @@ public class LicensesExpiryNotificationTests extends ESSingleNodeTestCase {
awaitNoBlock(client());
licensesService.register(licensee);
awaitNoPendingTasks(client());
boolean success = awaitBusy(() -> licensee.licenseStates.size() == 1);
assertTrue(dumpLicensingStates(licensee.licenseStates), success);
boolean success = awaitBusy(() -> licensee.statuses.size() == 1);
assertTrue(dumpLicensingStates(licensee.statuses), success);
registerAndAckSignedLicenses(licensesService, generateSignedLicense(TimeValue.timeValueSeconds(4)), LicensesStatus.VALID);
success = awaitBusy(() -> licensee.licenseStates.size() == 4);
success = awaitBusy(() -> licensee.statuses.size() == 4);
// trial: enable, signed: enable, signed: grace, signed: disabled
assertLicenseStates(licensee, LicenseState.ENABLED, LicenseState.ENABLED, LicenseState.GRACE_PERIOD, LicenseState.DISABLED);
assertTrue(dumpLicensingStates(licensee.licenseStates), success);
assertTrue(dumpLicensingStates(licensee.statuses), success);
licensesService.stop();
}
@ -102,10 +104,10 @@ public class LicensesExpiryNotificationTests extends ESSingleNodeTestCase {
registerAndAckSignedLicenses(licensesService, generateSignedLicense(TimeValue.timeValueSeconds(2)), LicensesStatus.VALID);
licensesService.register(licensee);
awaitNoPendingTasks(client());
boolean success = awaitBusy(() -> licensee.licenseStates.size() == 3);
boolean success = awaitBusy(() -> licensee.statuses.size() == 3);
// signed: enable, signed: grace, signed: disabled
assertLicenseStates(licensee, LicenseState.ENABLED, LicenseState.GRACE_PERIOD, LicenseState.DISABLED);
assertTrue(dumpLicensingStates(licensee.licenseStates), success);
assertTrue(dumpLicensingStates(licensee.statuses), success);
licensesService.stop();
}
@ -123,10 +125,10 @@ public class LicensesExpiryNotificationTests extends ESSingleNodeTestCase {
licensesService.register(licensee1);
licensesService.register(licensee2);
awaitNoPendingTasks(client());
boolean success = awaitBusy(() -> licensee1.licenseStates.size() == 3);
assertTrue(dumpLicensingStates(licensee1.licenseStates), success);
success = awaitBusy(() -> licensee2.licenseStates.size() == 3);
assertTrue(dumpLicensingStates(licensee2.licenseStates), success);
boolean success = awaitBusy(() -> licensee1.statuses.size() == 3);
assertTrue(dumpLicensingStates(licensee1.statuses), success);
success = awaitBusy(() -> licensee2.statuses.size() == 3);
assertTrue(dumpLicensingStates(licensee2.statuses), success);
// signed license: enable, grace, disabled
assertLicenseStates(licensee1, LicenseState.ENABLED, LicenseState.GRACE_PERIOD, LicenseState.DISABLED);
assertLicenseStates(licensee2, LicenseState.ENABLED, LicenseState.GRACE_PERIOD, LicenseState.DISABLED);
@ -145,18 +147,18 @@ public class LicensesExpiryNotificationTests extends ESSingleNodeTestCase {
licensesService.register(licensee);
awaitNoPendingTasks(client());
// trial license enabled
boolean success = awaitBusy(() -> licensee.licenseStates.size() == 1);
assertTrue(dumpLicensingStates(licensee.licenseStates), success);
boolean success = awaitBusy(() -> licensee.statuses.size() == 1);
assertTrue(dumpLicensingStates(licensee.statuses), success);
registerAndAckSignedLicenses(licensesService, generateSignedLicense("basic", TimeValue.timeValueSeconds(3)), LicensesStatus.VALID);
// signed license enabled
success = awaitBusy(() -> licensee.licenseStates.size() == 2);
assertTrue(dumpLicensingStates(licensee.licenseStates), success);
success = awaitBusy(() -> licensee.statuses.size() == 2);
assertTrue(dumpLicensingStates(licensee.statuses), success);
registerAndAckSignedLicenses(licensesService, generateSignedLicense("gold", TimeValue.timeValueSeconds(2)), LicensesStatus.VALID);
// second signed license enabled, grace and expired
success = awaitBusy(() ->licensee.licenseStates.size() == 5);
success = awaitBusy(() ->licensee.statuses.size() == 5);
assertLicenseStates(licensee, LicenseState.ENABLED, LicenseState.ENABLED, LicenseState.ENABLED, LicenseState.GRACE_PERIOD,
LicenseState.DISABLED);
assertTrue(dumpLicensingStates(licensee.licenseStates), success);
assertTrue(dumpLicensingStates(licensee.statuses), success);
licensesService.stop();
}
@ -171,33 +173,39 @@ public class LicensesExpiryNotificationTests extends ESSingleNodeTestCase {
awaitNoBlock(client());
licensesService.register(licensee);
// trial license: enabled, grace, disabled
boolean success = awaitBusy(() -> licensee.licenseStates.size() == 3);
boolean success = awaitBusy(() -> licensee.statuses.size() == 3);
assertTrue(dumpLicensingStates(licensee.licenseStates), success);
assertTrue(dumpLicensingStates(licensee.statuses), success);
// install license
registerAndAckSignedLicenses(licensesService, generateSignedLicense("basic", TimeValue.timeValueSeconds(2)), LicensesStatus.VALID);
// trial license: enabled, grace, disabled, signed license: enabled, grace, disabled
success = awaitBusy(() -> licensee.licenseStates.size() == 6);
success = awaitBusy(() -> licensee.statuses.size() == 6);
assertLicenseStates(licensee, LicenseState.ENABLED, LicenseState.GRACE_PERIOD, LicenseState.DISABLED, LicenseState.ENABLED,
LicenseState.GRACE_PERIOD, LicenseState.DISABLED);
assertTrue(dumpLicensingStates(licensee.licenseStates), success);
assertTrue(dumpLicensingStates(licensee.statuses), success);
licensesService.stop();
}
private void assertLicenseStates(AssertingLicensee licensee, LicenseState... states) {
StringBuilder msg = new StringBuilder();
msg.append("Actual: ");
msg.append(dumpLicensingStates(licensee.licenseStates));
msg.append(dumpLicensingStates(licensee.statuses));
msg.append(" Expected: ");
msg.append(dumpLicensingStates(states));
assertThat(msg.toString(), licensee.licenseStates.size(), equalTo(states.length));
assertThat(msg.toString(), licensee.statuses.size(), equalTo(states.length));
for (int i = 0; i < states.length; i++) {
assertThat(msg.toString(), licensee.licenseStates.get(i), equalTo(states[i]));
assertThat(msg.toString(), licensee.statuses.get(i), equalTo(states[i]));
}
}
private String dumpLicensingStates(List<LicenseState> states) {
return dumpLicensingStates(states.toArray(new LicenseState[states.size()]));
private String dumpLicensingStates(List<Licensee.Status> statuses) {
return dumpLicensingStates(statuses.toArray(new Licensee.Status[statuses.size()]));
}
private String dumpLicensingStates(Licensee.Status... statuses) {
return dumpLicensingStates((LicenseState[]) Arrays.asList(statuses).stream()
.map(Licensee.Status::getLicenseState).collect(Collectors.toList()).toArray());
}
private String dumpLicensingStates(LicenseState... states) {

View File

@ -6,9 +6,9 @@
package org.elasticsearch.license.plugin.core;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.cluster.ack.ClusterStateUpdateResponse;
import org.elasticsearch.cluster.metadata.MetaData;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.license.core.License;
import org.elasticsearch.license.plugin.TestUtils;
@ -20,9 +20,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
import static org.elasticsearch.license.plugin.TestUtils.generateSignedLicense;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
public class LicensesManagerServiceTests extends ESSingleNodeTestCase {
static {
MetaData.registerPrototype(LicensesMetaData.TYPE, LicensesMetaData.PROTO);
}
@ -111,6 +114,53 @@ public class LicensesManagerServiceTests extends ESSingleNodeTestCase {
assertThat(licensesMetaData.getLicense(), equalTo(LicensesMetaData.LICENSE_TOMBSTONE));
}
public void testRemoveLicensesAndLicenseeNotification() throws Exception {
LicensesService licensesService = getInstanceFromNode(LicensesService.class);
licensesService.start();
ClusterService clusterService = getInstanceFromNode(ClusterService.class);
// generate a trial license for one feature
TestUtils.AssertingLicensee licensee = new TestUtils.AssertingLicensee("", logger);
licensesService.register(licensee);
// we should get a trial license to begin with
assertBusy(new Runnable() {
@Override
public void run() {
assertThat(licensee.statuses, hasSize(1));
assertThat(licensee.statuses.get(0).getMode(), is(License.OperationMode.TRIAL));
assertThat(licensee.statuses.get(0).getLicenseState(), is(LicenseState.ENABLED));
}
});
// generate signed licenses
License license = generateSignedLicense(TimeValue.timeValueHours(1));
TestUtils.registerAndAckSignedLicenses(licensesService, license, LicensesStatus.VALID);
assertBusy(new Runnable() {
@Override
public void run() {
assertThat(licensee.statuses, hasSize(2));
assertThat(licensee.statuses.get(1).getMode(), not(License.OperationMode.TRIAL));
assertThat(licensee.statuses.get(1).getLicenseState(), is(LicenseState.ENABLED));
}
});
// remove signed licenses
removeAndAckSignedLicenses(licensesService);
assertBusy(new Runnable() {
@Override
public void run() {
assertThat(licensee.statuses, hasSize(3));
}
});
LicensesMetaData licensesMetaData = clusterService.state().metaData().custom(LicensesMetaData.TYPE);
assertThat(licensesMetaData.getLicense(), is(LicensesMetaData.LICENSE_TOMBSTONE));
assertThat(licensee.statuses, hasSize(3));
assertThat(licensee.statuses.get(2).getLicenseState(), is(LicenseState.DISABLED));
assertThat(licensee.statuses.get(2).getMode(), is(License.OperationMode.MISSING));
}
private void removeAndAckSignedLicenses(final LicensesService licensesService) {
final CountDownLatch latch = new CountDownLatch(1);
final AtomicBoolean success = new AtomicBoolean(false);

View File

@ -36,7 +36,7 @@ public class MonitoringFeatureSet implements XPackFeatureSet {
@Override
public boolean available() {
return licensee != null && licensee.available();
return licensee != null && licensee.isAvailable();
}
@Override

View File

@ -75,12 +75,12 @@ public class MonitoringLicensee extends AbstractLicenseeComponent<MonitoringLice
}
/**
* Monitoring is always available regardless of the license type (operation mode)
* Monitoring is always available as long as there is a valid license
*
* @return true
*/
public boolean available() {
return true;
public boolean isAvailable() {
return status.getMode() != OperationMode.MISSING && status.getLicenseState() != LicenseState.DISABLED;
}
/**

View File

@ -36,7 +36,7 @@ public class WatcherFeatureSet implements XPackFeatureSet {
@Override
public boolean available() {
return licensee != null && licensee.available();
return licensee != null && licensee.isAvailable();
}
@Override

View File

@ -62,12 +62,22 @@ public class WatcherLicensee extends AbstractLicenseeComponent<WatcherLicensee>
*
* @return {@code true} as long as the license is valid. Otherwise {@code false}.
*/
public boolean available() {
public boolean isAvailable() {
// status is volatile, so a local variable is used for a consistent view
Status localStatus = status;
return localStatus.getLicenseState() != LicenseState.DISABLED && (localStatus.getMode() == OperationMode.TRIAL ||
localStatus.getMode() == OperationMode.GOLD || localStatus.getMode() == OperationMode.PLATINUM);
if (localStatus.getLicenseState() == LicenseState.DISABLED) {
return false;
}
switch (localStatus.getMode()) {
case TRIAL:
case GOLD:
case PLATINUM:
return true;
default:
return false;
}
}
public boolean isExecutingActionsAllowed() {
@ -84,7 +94,7 @@ public class WatcherLicensee extends AbstractLicenseeComponent<WatcherLicensee>
public boolean isWatcherTransportActionAllowed() {
return available();
return isAvailable();
}