Skip to content

Commit

Permalink
Move license warning header calculation to license service (#76735)
Browse files Browse the repository at this point in the history
The license warning header is emitted when the license is close to
expiring and a licensed feature is checked. However, this calculation
and the resulting string must be recalculated on every license check.

This commit moves the formation of the warning into the license service,
and changes the license state to emit the fixed warning string. The
license service then rebuilds the message each day within the license
warning period.
  • Loading branch information
rjernst committed Sep 9, 2021
1 parent c1bee0b commit 4b4fc05
Show file tree
Hide file tree
Showing 17 changed files with 174 additions and 150 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import java.util.stream.Stream;
Expand Down Expand Up @@ -184,7 +185,7 @@ static CharSequence buildExpirationMessage(long expirationMillis, boolean expire
}

private void populateExpirationCallbacks() {
expirationCallbacks.add(new ExpirationCallback.Pre(days(7), days(25), days(1)) {
expirationCallbacks.add(new ExpirationCallback.Pre(days(0), days(25), days(1)) {
@Override
public void on(License license) {
logExpirationWarning(license.expiryDate(), false);
Expand Down Expand Up @@ -469,21 +470,34 @@ private void updateLicenseState(LicensesMetadata licensesMetadata) {
}
}

protected static String getExpiryWarning(long licenseExpiryDate, long currentTime) {
final long diff = licenseExpiryDate - currentTime;
if (LICENSE_EXPIRATION_WARNING_PERIOD.getMillis() > diff) {
final long days = TimeUnit.MILLISECONDS.toDays(diff);
final String expiryMessage = (days == 0 && diff > 0)? "expires today":
(diff > 0? String.format(Locale.ROOT, "will expire in [%d] days", days):
String.format(Locale.ROOT, "expired on [%s]", LicenseService.DATE_FORMATTER.formatMillis(licenseExpiryDate)));
return "Your license " + expiryMessage + ". " +
"Contact your administrator or update your license for continued use of features";
}
return null;
}

protected void updateLicenseState(final License license) {
long time = clock.millis();
if (license == LicensesMetadata.LICENSE_TOMBSTONE) {
// implies license has been explicitly deleted
licenseState.update(License.OperationMode.MISSING, false, license.expiryDate());
licenseState.update(License.OperationMode.MISSING,false, getExpiryWarning(license.expiryDate(), time));
return;
}
if (license != null) {
long time = clock.millis();
final boolean active;
if (license.expiryDate() == BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS) {
active = true;
} else {
active = time >= license.issueDate() && time < license.expiryDate();
}
licenseState.update(license.operationMode(), active, license.expiryDate());
licenseState.update(license.operationMode(), active, getExpiryWarning(license.expiryDate(), time));

if (active) {
logger.debug("license [{}] - valid", license.uid());
Expand Down Expand Up @@ -532,11 +546,17 @@ static SchedulerEngine.Schedule nextLicenseCheck(License license) {
// when we encounter a license with a future issue date
// which can happen with autogenerated license,
// we want to schedule a notification on the license issue date
// so the license is notificed once it is valid
// so the license is notified once it is valid
// see https://github.com/elastic/x-plugins/issues/983
return license.issueDate();
} else if (time < license.expiryDate()) {
return license.expiryDate();
// Re-check the license every day during the warning period up to the license expiration.
// This will cause the warning message to be updated that is emitted on soon-expiring license use.
long nextTime = license.expiryDate() - LICENSE_EXPIRATION_WARNING_PERIOD.getMillis();
while (nextTime <= time) {
nextTime += TimeValue.timeValueDays(1).getMillis();
}
return nextTime;
}
return -1; // license is expired, no need to check again
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
public interface LicenseStateListener {

/**
* Callback when the license state changes. See {@link XPackLicenseState#update(License.OperationMode, boolean, long)}.
* Callback when the license state changes. See {@link XPackLicenseState#update(License.OperationMode, boolean, String)}.
*/
void licenseStateChanged();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,12 @@
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.LongSupplier;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import static org.elasticsearch.license.LicenseService.LICENSE_EXPIRATION_WARNING_PERIOD;

/**
* A holder for the current state of the license for all xpack features.
*/
Expand Down Expand Up @@ -377,13 +374,13 @@ private static class Status {
/** True if the license is active, or false if it is expired. */
final boolean active;

/** The current expiration date of the license; Long.MAX_VALUE if not available yet. */
final long licenseExpiryDate;
/** A warning to be emitted on license checks about the license expiring soon. */
final String expiryWarning;

Status(OperationMode mode, boolean active, long licenseExpiryDate) {
Status(OperationMode mode, boolean active, String expiryWarning) {
this.mode = mode;
this.active = active;
this.licenseExpiryDate = licenseExpiryDate;
this.expiryWarning = expiryWarning;
}
}

Expand All @@ -402,7 +399,7 @@ private static class Status {
// XPackLicenseState. However, if status is read multiple times in a method, it can change in between
// reads. Methods should use `executeAgainstStatus` and `checkAgainstStatus` to ensure that the status
// is only read once.
private volatile Status status = new Status(OperationMode.TRIAL, true, Long.MAX_VALUE);
private volatile Status status = new Status(OperationMode.TRIAL, true, null);

public XPackLicenseState(LongSupplier epochMillisProvider) {
this.listeners = new CopyOnWriteArrayList<>();
Expand Down Expand Up @@ -437,10 +434,10 @@ private boolean checkAgainstStatus(Predicate<Status> statusPredicate) {
*
* @param mode The mode (type) of the current license.
* @param active True if the current license exists and is within its allowed usage period; false if it is expired or missing.
* @param expirationDate Expiration date of the current license.
* @param expiryWarning Warning to emit on license checks about the license expiring soon.
*/
protected void update(OperationMode mode, boolean active, long expirationDate) {
status = new Status(mode, active, expirationDate);
protected void update(OperationMode mode, boolean active, String expiryWarning) {
status = new Status(mode, active, expiryWarning);
listeners.forEach(LicenseStateListener::licenseStateChanged);
}

Expand Down Expand Up @@ -471,14 +468,14 @@ public boolean checkFeature(Feature feature) {
}

void featureUsed(LicensedFeature feature) {
checkExpiry();
usage.put(new FeatureUsage(feature, null), epochMillisProvider.getAsLong());
checkForExpiry(feature);
}

void enableUsageTracking(LicensedFeature feature, String contextName) {
checkExpiry();
Objects.requireNonNull(contextName, "Context name cannot be null");
usage.put(new FeatureUsage(feature, contextName), -1L);
checkForExpiry(feature);
}

void disableUsageTracking(LicensedFeature feature, String contextName) {
Expand Down Expand Up @@ -509,24 +506,13 @@ public boolean isAllowed(Feature feature) {

// Package protected: Only allowed to be called by LicensedFeature
boolean isAllowed(LicensedFeature feature) {
if (isAllowedByLicense(feature.getMinimumOperationMode(), feature.isNeedsActive())) {
return true;
}
return false;
}

private void checkForExpiry(LicensedFeature feature) {
final long licenseExpiryDate = getLicenseExpiryDate();
// TODO: this should use epochMillisProvider to avoid a system call + testability
final long diff = licenseExpiryDate - System.currentTimeMillis();
if (feature.getMinimumOperationMode().compareTo(OperationMode.BASIC) > 0 &&
LICENSE_EXPIRATION_WARNING_PERIOD.getMillis() > diff) {
final long days = TimeUnit.MILLISECONDS.toDays(diff);
final String expiryMessage = (days == 0 && diff > 0)? "expires today":
(diff > 0? String.format(Locale.ROOT, "will expire in [%d] days", days):
String.format(Locale.ROOT, "expired on [%s]", LicenseService.DATE_FORMATTER.formatMillis(licenseExpiryDate)));
HeaderWarning.addWarning("Your license {}. " +
"Contact your administrator or update your license for continued use of features", expiryMessage);
return isAllowedByLicense(feature.getMinimumOperationMode(), feature.isNeedsActive());
}

void checkExpiry() {
String warning = status.expiryWarning;
if (warning != null) {
HeaderWarning.addWarning(warning);
}
}

Expand Down Expand Up @@ -610,11 +596,6 @@ public boolean isAllowedByLicense(OperationMode minimumMode, boolean needActive)
});
}

/** Return the current license expiration date. */
public long getLicenseExpiryDate() {
return executeAgainstStatus(status -> status.licenseExpiryDate);
}

/**
* A convenient method to test whether a feature is by license status.
* @see #isAllowedByLicense(OperationMode, boolean)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
import org.elasticsearch.xpack.core.scheduler.SchedulerEngine;
import org.junit.Before;

import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.Locale;

import static org.hamcrest.Matchers.equalTo;

public class LicenseScheduleTests extends ESTestCase {
Expand All @@ -19,17 +23,11 @@ public class LicenseScheduleTests extends ESTestCase {
private SchedulerEngine.Schedule schedule;

@Before
public void setuo() throws Exception {
license = TestUtils.generateSignedLicense(TimeValue.timeValueHours(12));
public void setup() throws Exception {
license = TestUtils.generateSignedLicense(TimeValue.timeValueDays(12));
schedule = LicenseService.nextLicenseCheck(license);
}

public void testEnabledLicenseSchedule() throws Exception {
int expiryDuration = (int) (license.expiryDate() - license.issueDate());
long triggeredTime = license.issueDate() + between(0, expiryDuration);
assertThat(schedule.nextScheduledTimeAfter(license.issueDate(), triggeredTime), equalTo(license.expiryDate()));
}

public void testExpiredLicenseSchedule() throws Exception {
long triggeredTime = license.expiryDate() + randomIntBetween(1, 1000);
assertThat(schedule.nextScheduledTimeAfter(license.issueDate(), triggeredTime),
Expand All @@ -41,4 +39,24 @@ public void testInvalidLicenseSchedule() throws Exception {
assertThat(schedule.nextScheduledTimeAfter(triggeredTime, triggeredTime),
equalTo(license.issueDate()));
}

public void testDailyWarningPeriod() {

long millisInDay = TimeValue.timeValueDays(1).getMillis();
long warningOffset = LicenseService.LICENSE_EXPIRATION_WARNING_PERIOD.getMillis();
do {
long nextOffset = license.expiryDate() - warningOffset;
long triggeredTime = nextOffset + randomLongBetween(1, millisInDay);
long expectedTime = nextOffset + millisInDay;
long scheduledTime = schedule.nextScheduledTimeAfter(triggeredTime, triggeredTime);
assertThat(String.format(Locale.ROOT,"Incorrect schedule:\nexpected [%s]\ngot [%s]\ntriggered [%s]\nexpiry [%s]",
DateTimeFormatter.ISO_INSTANT.format(Instant.ofEpochMilli(expectedTime)),
DateTimeFormatter.ISO_INSTANT.format(Instant.ofEpochMilli(scheduledTime)),
DateTimeFormatter.ISO_INSTANT.format(Instant.ofEpochMilli(triggeredTime)),
DateTimeFormatter.ISO_INSTANT.format(Instant.ofEpochMilli(license.expiryDate()))),
scheduledTime, equalTo(expectedTime));

warningOffset -= millisInDay;
} while (warningOffset > 0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.elasticsearch.test.TestMatchers;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.watcher.ResourceWatcherService;
import org.hamcrest.Matchers;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;

Expand All @@ -43,8 +44,11 @@
import java.util.function.Consumer;
import java.util.stream.Collectors;

import static org.elasticsearch.license.LicenseService.LICENSE_EXPIRATION_WARNING_PERIOD;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.startsWith;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
Expand Down Expand Up @@ -200,4 +204,32 @@ private License buildLicense(License.LicenseType type, TimeValue expires) {
.signature(null)
.build();
}

private void assertExpiryWarning(long adjustment, String msg) {
long now = System.currentTimeMillis();
long expiration = now + adjustment;
String warning = LicenseService.getExpiryWarning(expiration, now);
if (msg == null) {
assertThat(warning, is(nullValue()));
} else {
assertThat(warning, Matchers.containsString(msg));
}
}

public void testNoExpiryWarning() {
assertExpiryWarning(LICENSE_EXPIRATION_WARNING_PERIOD.getMillis(), null);
}

public void testExpiryWarningSoon() {
assertExpiryWarning(LICENSE_EXPIRATION_WARNING_PERIOD.getMillis() - 1, "Your license will expire in [6] days");
}

public void testExpiryWarningToday() {
assertExpiryWarning(1, "Your license expires today");
}

public void testExpiryWarningExpired() {
assertExpiryWarning(0, "Your license expired on");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -363,22 +363,22 @@ public void onFailure(Exception e) {
public static class AssertingLicenseState extends XPackLicenseState {
public final List<License.OperationMode> modeUpdates = new ArrayList<>();
public final List<Boolean> activeUpdates = new ArrayList<>();
public final List<Long> expirationDateUpdates = new ArrayList<>();
public final List<String> expiryWarnings = new ArrayList<>();

public AssertingLicenseState() {
super(() -> 0);
}

@Override
protected void update(License.OperationMode mode, boolean active, long expirationDate) {
protected void update(License.OperationMode mode, boolean active, String expiryWarning) {
modeUpdates.add(mode);
activeUpdates.add(active);
expirationDateUpdates.add(expirationDate);
expiryWarnings.add(expiryWarning);
}
}

/**
* A license state that makes the {@link #update(License.OperationMode, boolean, long)}
* A license state that makes the {@link #update(License.OperationMode, boolean, String)}
* method public for use in tests.
*/
public static class UpdatableLicenseState extends XPackLicenseState {
Expand All @@ -391,8 +391,8 @@ public UpdatableLicenseState(Settings settings) {
}

@Override
public void update(License.OperationMode mode, boolean active, long expirationDate) {
super.update(mode, active, expirationDate);
public void update(License.OperationMode mode, boolean active, String expiryWarning) {
super.update(mode, active, expiryWarning);
}
}

Expand Down

0 comments on commit 4b4fc05

Please sign in to comment.