This is an automated email from the ASF dual-hosted git repository.
reschke pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/jackrabbit-oak.git
The following commit(s) were added to refs/heads/trunk by this push:
new 7c96ca531a Revert "OAK-11766 Write Throttling Mechanism -
Session.save() delay (#2339)"
7c96ca531a is described below
commit 7c96ca531a1f91c10423ad311a04aa28ace5bc8b
Author: Julian Reschke <[email protected]>
AuthorDate: Tue Jun 24 14:51:33 2025 +0100
Revert "OAK-11766 Write Throttling Mechanism - Session.save() delay (#2339)"
This reverts commit 201c1c894f233b16ed0559011fee32d21b0ce257.
---
.../oak/api/jmx/RepositoryManagementMBean.java | 16 -
.../jackrabbit/oak/api/jmx/package-info.java | 2 +-
.../oak/management/RepositoryManager.java | 11 -
.../oak/jcr/delegate/SessionDelegate.java | 9 +-
.../oak/jcr/repository/RepositoryImpl.java | 6 +-
.../oak/jcr/session/SessionSaveDelayer.java | 136 ----
.../oak/jcr/session/SessionSaveDelayerConfig.java | 288 ---------
.../oak/jcr/delegate/AbstractDelegatorTest.java | 4 +-
.../jcr/session/SessionSaveDelayerConfigTest.java | 691 ---------------------
.../oak/jcr/session/SessionSaveDelayerTest.java | 401 ------------
10 files changed, 4 insertions(+), 1560 deletions(-)
diff --git
a/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/RepositoryManagementMBean.java
b/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/RepositoryManagementMBean.java
index ae326dbc69..9c879ef4e3 100644
---
a/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/RepositoryManagementMBean.java
+++
b/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/RepositoryManagementMBean.java
@@ -263,20 +263,4 @@ public interface RepositoryManagementMBean {
@Description("Refresh all currently open sessions")
TabularData refreshAllSessions();
- /**
- * Get the Session.save() delay configuration.
- *
- * @return the configuration
- */
- @Description("The Session.save() delay configuration")
- String getSessionSaveDelayerConfig();
-
- /**
- * Set the Session.save() delay configuration.
- *
- * @param config the new configuration
- */
- @Description("The Session.save() delay configuration")
- void setSessionSaveDelayerConfig(String config);
-
}
diff --git
a/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/package-info.java
b/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/package-info.java
index 47361eda97..678163b257 100644
--- a/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/package-info.java
+++ b/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/package-info.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-@Version("4.15.0")
+@Version("4.14.0")
package org.apache.jackrabbit.oak.api.jmx;
import org.osgi.annotation.versioning.Version;
diff --git
a/oak-core/src/main/java/org/apache/jackrabbit/oak/management/RepositoryManager.java
b/oak-core/src/main/java/org/apache/jackrabbit/oak/management/RepositoryManager.java
index c465d270f0..738b2bde8f 100644
---
a/oak-core/src/main/java/org/apache/jackrabbit/oak/management/RepositoryManager.java
+++
b/oak-core/src/main/java/org/apache/jackrabbit/oak/management/RepositoryManager.java
@@ -56,7 +56,6 @@ import org.jetbrains.annotations.NotNull;
*/
public class RepositoryManager extends AnnotatedStandardMBean implements
RepositoryManagementMBean {
private final Whiteboard whiteboard;
- private String sessionSaveConfig;
public RepositoryManager(@NotNull Whiteboard whiteboard) {
super(RepositoryManagementMBean.class);
@@ -278,14 +277,4 @@ public class RepositoryManager extends
AnnotatedStandardMBean implements Reposit
}
}));
}
-
- @Override
- public String getSessionSaveDelayerConfig() {
- return sessionSaveConfig;
- }
-
- @Override
- public void setSessionSaveDelayerConfig(String config) {
- this.sessionSaveConfig = config;
- }
}
diff --git
a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/SessionDelegate.java
b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/SessionDelegate.java
index 34c3c70fb6..20b0b702b6 100644
---
a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/SessionDelegate.java
+++
b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/SessionDelegate.java
@@ -54,7 +54,6 @@ import org.apache.jackrabbit.oak.jcr.observation.EventFactory;
import org.apache.jackrabbit.oak.jcr.session.RefreshStrategy;
import org.apache.jackrabbit.oak.jcr.session.RefreshStrategy.Composite;
import org.apache.jackrabbit.oak.jcr.session.SessionNamespaces;
-import org.apache.jackrabbit.oak.jcr.session.SessionSaveDelayer;
import org.apache.jackrabbit.oak.jcr.session.SessionStats;
import org.apache.jackrabbit.oak.jcr.session.SessionStats.Counters;
import org.apache.jackrabbit.oak.jcr.session.operation.SessionOperation;
@@ -139,8 +138,6 @@ public class SessionDelegate {
private final SessionNamespaces namespaces;
- private final SessionSaveDelayer sessionSaveDelayer;
-
/**
* Create a new session delegate for a {@code ContentSession}. The refresh
behaviour of the
* session is governed by the value of the {@code refreshInterval}
argument: if the session
@@ -153,7 +150,6 @@ public class SessionDelegate {
* @param securityProvider the security provider
* @param refreshStrategy the refresh strategy used for auto refreshing
this session
* @param statisticManager the statistics manager for tracking session
operations
- * @param sessionSaveDelayer the session save delay mechanism
*/
public SessionDelegate(
@NotNull ContentSession contentSession,
@@ -161,8 +157,7 @@ public class SessionDelegate {
@NotNull RefreshStrategy refreshStrategy,
@NotNull ThreadLocal<Long> threadSaveCount,
@NotNull StatisticManager statisticManager,
- @NotNull Clock clock,
- @NotNull SessionSaveDelayer sessionSaveDelayer) {
+ @NotNull Clock clock) {
this.contentSession = requireNonNull(contentSession);
this.securityProvider = requireNonNull(securityProvider);
this.root = contentSession.getLatestRoot();
@@ -181,7 +176,6 @@ public class SessionDelegate {
readDuration = statisticManager.getTimer(SESSION_READ_DURATION);
writeCounter = statisticManager.getMeter(SESSION_WRITE_COUNTER);
writeDuration = statisticManager.getTimer(SESSION_WRITE_DURATION);
- this.sessionSaveDelayer = sessionSaveDelayer;
}
@NotNull
@@ -398,7 +392,6 @@ public class SessionDelegate {
if (userData != null) {
info.put(EventFactory.USER_DATA, userData);
}
- sessionSaveDelayer.delayIfNeeded(userData);
root.commit(Collections.unmodifiableMap(info));
if (permissionProvider != null && refreshPermissionProvider) {
permissionProvider.refresh();
diff --git
a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/repository/RepositoryImpl.java
b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/repository/RepositoryImpl.java
index d5b9edd4bd..b7f607b33b 100644
---
a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/repository/RepositoryImpl.java
+++
b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/repository/RepositoryImpl.java
@@ -54,7 +54,6 @@ import org.apache.jackrabbit.oak.jcr.delegate.SessionDelegate;
import org.apache.jackrabbit.oak.jcr.session.RefreshStrategy;
import org.apache.jackrabbit.oak.jcr.session.RefreshStrategy.Composite;
import org.apache.jackrabbit.oak.jcr.session.SessionContext;
-import org.apache.jackrabbit.oak.jcr.session.SessionSaveDelayer;
import org.apache.jackrabbit.oak.jcr.session.SessionStats;
import org.apache.jackrabbit.oak.jcr.version.FrozenNodeLogger;
import org.apache.jackrabbit.oak.plugins.observation.CommitRateLimiter;
@@ -121,7 +120,6 @@ public class RepositoryImpl implements JackrabbitRepository
{
private final MountInfoProvider mountInfoProvider;
private final BlobAccessProvider blobAccessProvider;
private final SessionQuerySettingsProvider sessionQuerySettingsProvider;
- private final SessionSaveDelayer sessionSaveDelayer;
/**
* {@link ThreadLocal} counter that keeps track of the save operations
@@ -178,7 +176,6 @@ public class RepositoryImpl implements JackrabbitRepository
{
this.frozenNodeLogger = new FrozenNodeLogger(clock, whiteboard);
this.sessionQuerySettingsProvider =
Optional.ofNullable(WhiteboardUtils.getService(whiteboard,
SessionQuerySettingsProvider.class))
.orElseGet(() -> new
FastQuerySizeSettingsProvider(fastQueryResultSize));
- this.sessionSaveDelayer = new SessionSaveDelayer(whiteboard);
}
//---------------------------------------------------------< Repository
>---
@@ -327,7 +324,7 @@ public class RepositoryImpl implements JackrabbitRepository
{
return new SessionDelegate(
contentSession, securityProvider, refreshStrategy,
- threadSaveCount, statisticManager, clock, sessionSaveDelayer) {
+ threadSaveCount, statisticManager, clock) {
// Defer session MBean registration to avoid cluttering the
// JMX name space with short lived sessions
@@ -357,7 +354,6 @@ public class RepositoryImpl implements JackrabbitRepository
{
statisticManager.dispose();
gcMonitorRegistration.unregister();
frozenNodeLogger.close();
- sessionSaveDelayer.close();
clock.close();
new ExecutorCloser(scheduledExecutor).close();
if (contentRepository instanceof Closeable) {
diff --git
a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/session/SessionSaveDelayer.java
b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/session/SessionSaveDelayer.java
deleted file mode 100644
index 2a9d1f1695..0000000000
---
a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/session/SessionSaveDelayer.java
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.jackrabbit.oak.jcr.session;
-
-import static org.apache.jackrabbit.oak.spi.toggle.Feature.newFeature;
-
-import java.io.Closeable;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-import org.apache.jackrabbit.guava.common.base.Strings;
-import org.apache.jackrabbit.oak.api.jmx.RepositoryManagementMBean;
-import org.apache.jackrabbit.oak.spi.toggle.Feature;
-import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard;
-import org.apache.jackrabbit.oak.spi.whiteboard.WhiteboardUtils;
-import org.jetbrains.annotations.NotNull;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * A delay mechanism for Session.save() operations. By default, Session.save
- * calls are not delayed. If enabled, some of the save() operations can be
- * delayed for a certain number of microseconds.
- *
- * This facility is enabled / disabled via feature toggle, and controlled via
- * JMX bean, or (for testing) via two system properties. There is no attempt to
- * control the delay, or which threads to delay, from within. It is meant for
- * emergency situation, specially for cases where some threads write too much.
- */
-public class SessionSaveDelayer implements Closeable {
-
- private static final Logger LOG =
LoggerFactory.getLogger(SessionSaveDelayer.class);
-
- private static final String FT_SAVE_DELAY_NAME = "FT_SAVE_DELAY_OAK-11766";
- private static final String ENABLED_PROP_NAME = "oak.sessionSaveDelayer";
- private static final String CONFIG_PROP_NAME =
"oak.sessionSaveDelayerConfig";
-
- private final boolean enabledViaSysPropertey =
Boolean.getBoolean(ENABLED_PROP_NAME);
- private final String sysPropertyConfig =
System.getProperty(CONFIG_PROP_NAME, "");
- private final Feature feature;
- private final Whiteboard whiteboard;
- private final AtomicBoolean closed = new AtomicBoolean();
-
- private RepositoryManagementMBean cachedMbean;
- private String lastConfigJson;
- private SessionSaveDelayerConfig lastConfig;
- private volatile boolean logNextDelay;
-
- public SessionSaveDelayer(@NotNull Whiteboard whiteboard) {
- this.feature = newFeature(FT_SAVE_DELAY_NAME, whiteboard);
- LOG.info("Initialized");
- if (enabledViaSysPropertey) {
- LOG.info("Enabled via system property: " + ENABLED_PROP_NAME);
- }
- this.whiteboard = whiteboard;
- }
-
- private RepositoryManagementMBean getRepositoryMBean() {
- if (cachedMbean == null) {
- cachedMbean = WhiteboardUtils.getService(whiteboard,
RepositoryManagementMBean.class);
- }
- return cachedMbean;
- }
-
- public long delayIfNeeded(String userData) {
- if (closed.get() || (!feature.isEnabled() && !enabledViaSysPropertey))
{
- return 0;
- }
- String config = sysPropertyConfig;
- RepositoryManagementMBean mbean = getRepositoryMBean();
- if (mbean != null) {
- String jmxConfig = mbean.getSessionSaveDelayerConfig();
- if (!Strings.isNullOrEmpty(jmxConfig)) {
- config = jmxConfig;
- }
- }
- if (Strings.isNullOrEmpty(config)) {
- return 0;
- }
- if (!config.equals(lastConfigJson)) {
- logNextDelay = true;
- lastConfigJson = config;
- try {
- // reset, if already set
- lastConfig = null;
- lastConfig = SessionSaveDelayerConfig.fromJson(config);
- LOG.info("New config: {}", lastConfig.toString());
- } catch (IllegalArgumentException e) {
- LOG.warn("Can not parse config {}", e);
- // don't delay
- return 0;
- }
- }
- if (lastConfig == null) {
- return 0;
- }
- String threadName = Thread.currentThread().getName();
- long delayNanos = lastConfig.getDelayNanos(threadName, userData, null);
- if (delayNanos > 0) {
- long millis = delayNanos / 1_000_000;
- int nanos = (int) (delayNanos % 1_000_000);
- if (logNextDelay) {
- LOG.info("Sleep {} ms {} ns for user {}", millis, nanos,
userData);
- logNextDelay = false;
- }
- try {
- Thread.sleep(millis, nanos);
- } catch (InterruptedException e) {
- // ignore
- Thread.currentThread().interrupt();
- }
- }
- return delayNanos;
- }
-
- @Override
- public void close() {
- if (!closed.getAndSet(true)) {
- feature.close();
- }
- }
-
-}
diff --git
a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/session/SessionSaveDelayerConfig.java
b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/session/SessionSaveDelayerConfig.java
deleted file mode 100644
index e89614c103..0000000000
---
a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/session/SessionSaveDelayerConfig.java
+++ /dev/null
@@ -1,288 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.jackrabbit.oak.jcr.session;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.regex.Pattern;
-import java.util.regex.PatternSyntaxException;
-
-import org.apache.jackrabbit.guava.common.base.Strings;
-import org.apache.jackrabbit.oak.commons.json.JsonObject;
-import org.apache.jackrabbit.oak.commons.json.JsopBuilder;
-import org.apache.jackrabbit.oak.commons.json.JsopTokenizer;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Configuration parser for a session save delay JSON configuration:
- * <pre>
- * {
- * "entries": [
- * {
- * "delayMillis": 1.0,
- * "threadNameRegex": "thread-.*",
- * "stackTraceRegex": ".*SomeClass.*"
- * },
- * {
- * "delayMillis": 0.5,
- * "threadNameRegex": "worker-\\d+"
- * }
- * ]
- * }
- * </pre>
- */
-public class SessionSaveDelayerConfig {
-
- private static final Logger LOG =
LoggerFactory.getLogger(SessionSaveDelayerConfig.class);
-
- private final List<DelayEntry> entries;
-
- public SessionSaveDelayerConfig(@NotNull List<DelayEntry> entries) {
- this.entries = new ArrayList<>(entries);
- }
-
- @NotNull
- public static SessionSaveDelayerConfig fromJson(@NotNull String
jsonConfig) throws IllegalArgumentException {
- if (Strings.isNullOrEmpty(jsonConfig)) {
- return new SessionSaveDelayerConfig(List.of());
- }
- try {
- JsopTokenizer tokenizer = new JsopTokenizer(jsonConfig);
- tokenizer.read('{');
- JsonObject root = JsonObject.create(tokenizer);
- List<DelayEntry> entries = new ArrayList<>();
- String entriesJson = root.getProperties().get("entries");
- if (entriesJson != null) {
- JsopTokenizer entryTokenizer = new JsopTokenizer(entriesJson);
- entryTokenizer.read('[');
- if (!entryTokenizer.matches(']')) {
- do {
- if (entryTokenizer.matches('{')) {
- DelayEntry entry =
parseDelayEntry(JsonObject.create(entryTokenizer));
- if (entry != null) {
- entries.add(entry);
- }
- } else {
- throw new IllegalArgumentException("Expected
object in entries array");
- }
- } while (entryTokenizer.matches(','));
- entryTokenizer.read(']');
- }
- }
- return new SessionSaveDelayerConfig(entries);
- } catch (Exception e) {
- throw new IllegalArgumentException("Failed to parse JSON
configuration: " + e.getMessage(), e);
- }
- }
-
- public List<DelayEntry> getEntries() {
- return entries;
- }
-
- public long getDelayNanos(@NotNull String threadName, @Nullable String
userData, @Nullable String stackTrace) {
- for (DelayEntry d : entries) {
- if (d.matches(threadName, userData, stackTrace)) {
- return d.getDelayNanos();
- }
- }
- return 0;
- }
-
- @Nullable
- private static DelayEntry parseDelayEntry(JsonObject entryObj) {
- String delayMillis = entryObj.getProperties().get("delayMillis");
- String threadNameRegex =
entryObj.getProperties().get("threadNameRegex");
- String userDataRegex = entryObj.getProperties().get("userDataRegex");
- String stackTraceRegex =
entryObj.getProperties().get("stackTraceRegex");
- String maxSavesPerSecond =
entryObj.getProperties().get("maxSavesPerSecond");
- if (delayMillis == null || threadNameRegex == null) {
- LOG.warn("Skipping entry with missing required fields (delay or
threadNameRegex)");
- return null;
- }
- try {
- double delay = Double.parseDouble(delayMillis);
- if (delay < 0) {
- LOG.warn("Skipping entry with negative delay");
- return null;
- }
- double maxSaves = 0.0;
- if (maxSavesPerSecond != null) {
- maxSaves = Double.parseDouble(maxSavesPerSecond);
- if (maxSaves < 0) {
- LOG.warn("Skipping entry with negative maxSavesPerSecond");
- return null;
- }
- }
- Pattern threadPattern =
Pattern.compile(JsopTokenizer.decodeQuoted(threadNameRegex));
- Pattern stackPattern = null;
- if (stackTraceRegex != null) {
- stackPattern =
Pattern.compile(JsopTokenizer.decodeQuoted(stackTraceRegex));
- }
- Pattern userDataPattern = null;
- if (userDataRegex != null) {
- userDataPattern =
Pattern.compile(JsopTokenizer.decodeQuoted(userDataRegex));
- }
- return new DelayEntry(delay, threadPattern, userDataPattern,
stackPattern, maxSaves);
- } catch (NumberFormatException e) {
- LOG.warn("Skipping entry with invalid delay value or
maxSavesPerSecond: {}", e.getMessage());
- return null;
- } catch (PatternSyntaxException e) {
- LOG.warn("Skipping entry with invalid regex pattern: {}",
e.getMessage());
- return null;
- }
- }
-
- @Override
- public String toString() {
- JsopBuilder json = new JsopBuilder();
- json.object().key("entries").array();
- for (DelayEntry entry : entries) {
- entry.toJson(json);
- }
- json.endArray().endObject();
- return JsopBuilder.prettyPrint(json.toString());
- }
-
- /**
- * Gets the stack trace of the current thread as a string.
- *
- * @return the current stack trace as a formatted string, or null if no
stack trace is available
- */
- @Nullable
- public static String getCurrentStackTrace() {
- StackTraceElement[] stackTrace =
Thread.currentThread().getStackTrace();
- if (stackTrace == null || stackTrace.length == 0) {
- return null;
- }
- StringBuilder sb = new StringBuilder();
- for (int i = 0; i < stackTrace.length; i++) {
- if (i > 0) {
- sb.append("\n\tat ");
- } else {
- sb.append("at ");
- }
- sb.append(stackTrace[i]);
- }
- return sb.toString();
- }
-
- public static class DelayEntry {
- private final long baseDelayNanos;
- private final Pattern threadNamePattern;
- private final Pattern stackTracePattern;
- private final Pattern userDataPattern;
- private final double maxSavesPerSecond;
- private final AtomicLong lastMatch = new AtomicLong(0);
-
- public DelayEntry(double delayMillis, @NotNull Pattern
threadNamePattern, @Nullable Pattern userDataPattern, @Nullable Pattern
stackTracePattern, double maxSavesPerSecond) {
- this.baseDelayNanos = (long) (delayMillis * 1_000_000);
- this.threadNamePattern = threadNamePattern;
- this.userDataPattern = userDataPattern;
- this.stackTracePattern = stackTracePattern;
- this.maxSavesPerSecond = maxSavesPerSecond;
- }
-
- public long getDelayNanos() {
- long totalDelayNanos = baseDelayNanos;
- if (maxSavesPerSecond > 0) {
- long currentTime = System.currentTimeMillis();
- double intervalMs = 1000.0 / maxSavesPerSecond;
- long lastMatchTime = lastMatch.get();
- if (lastMatchTime > 0) {
- long nextAllowedTime = lastMatchTime + (long) intervalMs;
- if (currentTime < nextAllowedTime) {
- long rateLimitDelayMs = nextAllowedTime - currentTime;
- totalDelayNanos += rateLimitDelayMs * 1_000_000;
- }
- }
- lastMatch.set(currentTime);
- }
- return totalDelayNanos;
- }
-
- public long getBaseDelayNanos() {
- return baseDelayNanos;
- }
-
- public double getMaxSavesPerSecond() {
- return maxSavesPerSecond;
- }
-
- @NotNull
- public Pattern getThreadNamePattern() {
- return threadNamePattern;
- }
-
- @Nullable
- public Pattern getStackTracePattern() {
- return stackTracePattern;
- }
-
- @Nullable
- public Pattern getUserDataPattern() {
- return userDataPattern;
- }
-
- boolean matches(@NotNull String threadName, @Nullable String userData,
@Nullable String stackTrace) {
- if (!threadNamePattern.matcher(threadName).matches()) {
- return false;
- }
- if (userDataPattern != null) {
- if (userData == null) {
- return false;
- }
- if (!userDataPattern.matcher(userData).find()) {
- return false;
- }
- }
- if (stackTracePattern != null) {
- if (stackTrace == null) {
- stackTrace =
SessionSaveDelayerConfig.getCurrentStackTrace();
- }
- return stackTracePattern.matcher(stackTrace).find();
- }
- return true;
- }
-
- @Override
- public String toString() {
- return toJson(new JsopBuilder()).toString();
- }
-
- public JsopBuilder toJson(JsopBuilder json) {
- json.object();
- double delayMillis = baseDelayNanos / 1_000_000.0;
- json.key("delayMillis").encodedValue(Double.toString(delayMillis));
- json.key("threadNameRegex").value(threadNamePattern.pattern());
- if (userDataPattern != null) {
- json.key("userDataRegex").value(userDataPattern.pattern());
- }
- if (stackTracePattern != null) {
- json.key("stackTraceRegex").value(stackTracePattern.pattern());
- }
- if (maxSavesPerSecond > 0) {
-
json.key("maxSavesPerSecond").encodedValue(Double.toString(maxSavesPerSecond));
- }
- return json.endObject();
- }
-
- }
-}
diff --git
a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/delegate/AbstractDelegatorTest.java
b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/delegate/AbstractDelegatorTest.java
index 649d0a5686..68a35e9fc8 100644
---
a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/delegate/AbstractDelegatorTest.java
+++
b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/delegate/AbstractDelegatorTest.java
@@ -22,7 +22,6 @@ import org.apache.jackrabbit.oak.api.ContentSession;
import org.apache.jackrabbit.oak.api.Root;
import org.apache.jackrabbit.oak.api.Tree;
import org.apache.jackrabbit.oak.jcr.session.RefreshStrategy;
-import org.apache.jackrabbit.oak.jcr.session.SessionSaveDelayer;
import org.apache.jackrabbit.oak.spi.security.SecurityProvider;
import
org.apache.jackrabbit.oak.spi.security.authorization.AuthorizationConfiguration;
import
org.apache.jackrabbit.oak.spi.security.authorization.permission.PermissionAware;
@@ -64,8 +63,7 @@ public abstract class AbstractDelegatorTest {
Whiteboard wb = new DefaultWhiteboard();
StatisticManager statisticManager = new StatisticManager(wb,
executorService);
return spy(new SessionDelegate(mockContentSession(root),
mockSecurityProvider(root, pp),
- RefreshStrategy.Composite.create(), new ThreadLocal<>(),
statisticManager, new Clock.Virtual(),
- new SessionSaveDelayer(wb)));
+ RefreshStrategy.Composite.create(), new ThreadLocal<>(),
statisticManager, new Clock.Virtual()));
}
@NotNull
diff --git
a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/session/SessionSaveDelayerConfigTest.java
b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/session/SessionSaveDelayerConfigTest.java
deleted file mode 100644
index 2e0120ff6c..0000000000
---
a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/session/SessionSaveDelayerConfigTest.java
+++ /dev/null
@@ -1,691 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.jackrabbit.oak.jcr.session;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import java.util.List;
-
-import org.junit.Test;
-
-/**
- * Tests for {@link SessionSaveDelayerConfig}.
- */
-public class SessionSaveDelayerConfigTest {
-
- @Test
- public void testEmptyConfiguration() {
- SessionSaveDelayerConfig config =
SessionSaveDelayerConfig.fromJson("");
- assertNotNull(config);
- assertTrue(config.getEntries().isEmpty());
- }
-
- @Test
- public void testNullConfiguration() {
- SessionSaveDelayerConfig config =
SessionSaveDelayerConfig.fromJson(null);
- assertNotNull(config);
- assertTrue(config.getEntries().isEmpty());
- }
-
- @Test
- public void testBasicConfiguration() {
- String json = "{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": 0.5,\n" +
- " \"threadNameRegex\": \"worker-\\\\d+\"\n" +
- " },\n" +
- " {\n" +
- " \"delayMillis\": 1,\n" +
- " \"threadNameRegex\": \"thread-.*\",\n" +
- " \"stackTraceRegex\": \".*SomeClass.*\"\n" +
- " }\n" +
- " ]\n" +
- "}";
-
- SessionSaveDelayerConfig config =
SessionSaveDelayerConfig.fromJson(json);
- assertNotNull(config);
-
- List<SessionSaveDelayerConfig.DelayEntry> entries =
config.getEntries();
- assertEquals(2, entries.size());
-
- SessionSaveDelayerConfig.DelayEntry first = entries.get(0);
- assertEquals(500_000L, first.getDelayNanos());
- assertEquals("worker-\\d+", first.getThreadNamePattern().pattern());
- assertNull(first.getStackTracePattern());
-
- SessionSaveDelayerConfig.DelayEntry second = entries.get(1);
- assertEquals(1_000_000L, second.getDelayNanos());
- assertEquals("thread-.*", second.getThreadNamePattern().pattern());
- assertNotNull(second.getStackTracePattern());
- assertEquals(".*SomeClass.*", second.getStackTracePattern().pattern());
- }
-
- @Test
- public void testEntryMatching() {
- String json = "{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": 1,\n" +
- " \"threadNameRegex\": \"thread-.*\",\n" +
- " \"stackTraceRegex\": \".*SomeClass.*\"\n" +
- " },\n" +
- " {\n" +
- " \"delayMillis\": 0.5,\n" +
- " \"threadNameRegex\": \"worker-\\\\d+\"\n" +
- " }\n" +
- " ]\n" +
- "}";
-
- SessionSaveDelayerConfig config =
SessionSaveDelayerConfig.fromJson(json);
- List<SessionSaveDelayerConfig.DelayEntry> entries =
config.getEntries();
-
- SessionSaveDelayerConfig.DelayEntry first = entries.get(0);
- SessionSaveDelayerConfig.DelayEntry second = entries.get(1);
-
- assertTrue(first.matches("thread-123", null, "at
com.example.SomeClass.method()"));
- assertTrue(first.matches("thread-abc", null, "SomeClass is here"));
- assertFalse(first.matches("thread-123", null, "no matching class"));
- assertFalse(first.matches("thread-123", null, null));
- assertFalse(first.matches("worker-123", null, "at
com.example.SomeClass.method()"));
-
- assertTrue(second.matches("worker-123", null, "any stack trace"));
- assertTrue(second.matches("worker-456", null, null));
- assertFalse(second.matches("worker-abc", null, "any stack trace"));
- assertFalse(second.matches("thread-123", null, "any stack trace"));
- }
-
- @Test
- public void testConfigurationWithMissingFields() {
- String json = "{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": 1,\n" +
- " \"threadNameRegex\": \"thread-.*\"\n" +
- " },\n" +
- " {\n" +
- " \"threadNameRegex\": \"worker-\\\\d+\"\n" +
- " },\n" +
- " {\n" +
- " \"delayMillis\": 0.5\n" +
- " }\n" +
- " ]\n" +
- "}";
-
- SessionSaveDelayerConfig config =
SessionSaveDelayerConfig.fromJson(json);
- assertNotNull(config);
-
- List<SessionSaveDelayerConfig.DelayEntry> entries =
config.getEntries();
- // Only the first entry should be valid (has both delay and
threadNameRegex)
- assertEquals(1, entries.size());
-
- SessionSaveDelayerConfig.DelayEntry entry = entries.get(0);
- assertEquals(1000_000L, entry.getDelayNanos());
- assertEquals("thread-.*", entry.getThreadNamePattern().pattern());
- assertNull(entry.getStackTracePattern());
- }
-
- @Test
- public void testConfigurationWithInvalidValues() {
- String json = "{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": \"invalid\",\n" +
- " \"threadNameRegex\": \"thread-.*\"\n" +
- " },\n" +
- " {\n" +
- " \"delayMillis\": -100,\n" +
- " \"threadNameRegex\": \"worker-\\\\d+\"\n" +
- " },\n" +
- " {\n" +
- " \"delay\": 500,\n" +
- " \"threadNameRegex\": \"[invalid-regex\"\n" +
- " }\n" +
- " ]\n" +
- "}";
-
- SessionSaveDelayerConfig config =
SessionSaveDelayerConfig.fromJson(json);
- assertNotNull(config);
-
- // All entries should be invalid and skipped
- assertTrue(config.getEntries().isEmpty());
- }
-
- @Test
- public void testEmptyEntriesArray() {
- String json = "{\n" +
- " \"entries\": []\n" +
- "}";
-
- SessionSaveDelayerConfig config =
SessionSaveDelayerConfig.fromJson(json);
- assertNotNull(config);
- assertTrue(config.getEntries().isEmpty());
- }
-
- @Test
- public void testConfigurationWithoutEntriesProperty() {
- String json = "{\n" +
- " \"someOtherProperty\": \"value\"\n" +
- "}";
-
- SessionSaveDelayerConfig config =
SessionSaveDelayerConfig.fromJson(json);
- assertNotNull(config);
- assertTrue(config.getEntries().isEmpty());
- }
-
- @Test
- public void testInvalidJsonThrowsException() {
- String invalidJson = "{ invalid json }";
-
- try {
- SessionSaveDelayerConfig.fromJson(invalidJson);
- fail("Expected IllegalArgumentException");
- } catch (IllegalArgumentException e) {
- assertTrue(e.getMessage().contains("Failed to parse JSON
configuration"));
- }
- }
-
- @Test
- public void testDelayConfigToString() {
- String json = "{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": 1.0,\n" +
- " \"threadNameRegex\": \"thread-.*\",\n" +
- " \"stackTraceRegex\": \".*SomeClass.*\"\n" +
- " }\n" +
- " ]\n" +
- "}";
-
- SessionSaveDelayerConfig config =
SessionSaveDelayerConfig.fromJson(json);
-
- assertEquals("{\n"
- + " \"entries\": [{\n"
- + " \"delayMillis\": 1.0, \"threadNameRegex\":
\"thread-.*\", \"stackTraceRegex\": \".*SomeClass.*\"\n"
- + " }]\n"
- + "}", config.toString());
- }
-
- @Test
- public void testComplexRegexPatterns() {
- String json = "{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": 2,\n" +
- " \"threadNameRegex\":
\"(?i)pool-\\\\d+-thread-\\\\d+\",\n" +
- " \"stackTraceRegex\":
\".*\\\\.(save|update|delete)\\\\(.*\\\\).*\"\n" +
- " }\n" +
- " ]\n" +
- "}";
-
- SessionSaveDelayerConfig config =
SessionSaveDelayerConfig.fromJson(json);
- assertNotNull(config);
-
- List<SessionSaveDelayerConfig.DelayEntry> entries =
config.getEntries();
- assertEquals(1, entries.size());
-
- SessionSaveDelayerConfig.DelayEntry entry = entries.get(0);
- assertEquals(2_000_000L, entry.getDelayNanos());
-
- // Test case-insensitive thread name matching
- assertTrue(entry.matches("pool-1-thread-5", null, "at
com.example.Service.save()"));
- assertTrue(entry.matches("POOL-2-THREAD-10", null, "at
com.example.Service.update()"));
-
- // Test stack trace pattern matching
- assertTrue(entry.matches("pool-1-thread-1", null, "at
com.example.Repository.delete(Repository.java:100)"));
- assertFalse(entry.matches("pool-1-thread-1", null, "at
com.example.Service.get()"));
- }
-
- @Test
- public void testUserDataPatternBasic() {
- String json = "{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": 1.0,\n" +
- " \"threadNameRegex\": \".*\",\n" +
- " \"userDataRegex\": \"admin.*\"\n" +
- " }\n" +
- " ]\n" +
- "}";
-
- SessionSaveDelayerConfig config =
SessionSaveDelayerConfig.fromJson(json);
- assertNotNull(config);
-
- List<SessionSaveDelayerConfig.DelayEntry> entries =
config.getEntries();
- assertEquals(1, entries.size());
-
- SessionSaveDelayerConfig.DelayEntry entry = entries.get(0);
- assertEquals(1_000_000L, entry.getDelayNanos());
-
- // Test userDataPattern matching
- assertTrue(entry.matches("any-thread", "admin", null));
- assertTrue(entry.matches("any-thread", "admin123", null));
- assertTrue(entry.matches("any-thread", "adminUser", null));
- assertFalse(entry.matches("any-thread", "user", null));
- assertFalse(entry.matches("any-thread", "testAdmin", null));
- assertFalse(entry.matches("any-thread", null, null)); // null userData
should not match
- }
-
- @Test
- public void testUserDataPatternWithComplexRegex() {
- String json = "{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": 0.5,\n" +
- " \"threadNameRegex\": \"worker-.*\",\n" +
- " \"userDataRegex\":
\"(admin|root|system)@.*\\\\.com$\"\n" +
- " }\n" +
- " ]\n" +
- "}";
-
- SessionSaveDelayerConfig config =
SessionSaveDelayerConfig.fromJson(json);
- assertNotNull(config);
-
- List<SessionSaveDelayerConfig.DelayEntry> entries =
config.getEntries();
- assertEquals(1, entries.size());
-
- SessionSaveDelayerConfig.DelayEntry entry = entries.get(0);
-
- // Test complex userDataPattern matching
- assertTrue(entry.matches("worker-1", "[email protected]", null));
- assertTrue(entry.matches("worker-2", "[email protected]", null));
- assertTrue(entry.matches("worker-3", "[email protected]", null));
- assertFalse(entry.matches("worker-1", "[email protected]", null));
- assertFalse(entry.matches("worker-1", "[email protected]", null));
- assertFalse(entry.matches("worker-1", "admin", null));
- assertFalse(entry.matches("other-thread", "[email protected]", null));
// thread name doesn't match
- }
-
- @Test
- public void testUserDataPatternCombinedWithStackTrace() {
- String json = "{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": 2.0,\n" +
- " \"threadNameRegex\": \".*\",\n" +
- " \"userDataRegex\": \"privileged.*\",\n" +
- " \"stackTraceRegex\": \".*Session\\\\.save.*\"\n" +
- " }\n" +
- " ]\n" +
- "}";
-
- SessionSaveDelayerConfig config =
SessionSaveDelayerConfig.fromJson(json);
- assertNotNull(config);
-
- List<SessionSaveDelayerConfig.DelayEntry> entries =
config.getEntries();
- assertEquals(1, entries.size());
-
- SessionSaveDelayerConfig.DelayEntry entry = entries.get(0);
-
- // All conditions must match
- assertTrue(entry.matches("any-thread", "privilegedUser", "at
javax.jcr.Session.save(Session.java:123)"));
- assertFalse(entry.matches("any-thread", "privilegedUser", "at
javax.jcr.Session.refresh(Session.java:456)")); // stack trace doesn't match
- assertFalse(entry.matches("any-thread", "normalUser", "at
javax.jcr.Session.save(Session.java:123)")); // userData doesn't match
- assertFalse(entry.matches("any-thread", null, "at
javax.jcr.Session.save(Session.java:123)")); // null userData
- }
-
- @Test
- public void testUserDataPatternMultipleEntries() {
- String json = "{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": 0.1,\n" +
- " \"threadNameRegex\": \".*\",\n" +
- " \"userDataRegex\": \"admin.*\"\n" +
- " },\n" +
- " {\n" +
- " \"delayMillis\": 0.2,\n" +
- " \"threadNameRegex\": \".*\",\n" +
- " \"userDataRegex\": \"guest.*\"\n" +
- " },\n" +
- " {\n" +
- " \"delayMillis\": 0.3,\n" +
- " \"threadNameRegex\": \".*\"\n" +
- " }\n" +
- " ]\n" +
- "}";
-
- SessionSaveDelayerConfig config =
SessionSaveDelayerConfig.fromJson(json);
-
- // Test that first matching entry is used
- assertEquals(100_000L, config.getDelayNanos("thread-1", "admin",
null));
- assertEquals(200_000L, config.getDelayNanos("thread-1", "guest123",
null));
- assertEquals(300_000L, config.getDelayNanos("thread-1", "normalUser",
null)); // matches third entry (no userData pattern)
- assertEquals(300_000L, config.getDelayNanos("thread-1", null, null));
// matches third entry (no userData pattern)
- }
-
- @Test
- public void testUserDataPatternWithInvalidRegex() {
- String json = "{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": 1.0,\n" +
- " \"threadNameRegex\": \".*\",\n" +
- " \"userDataRegex\": \"[invalid-regex\"\n" +
- " }\n" +
- " ]\n" +
- "}";
-
- SessionSaveDelayerConfig config =
SessionSaveDelayerConfig.fromJson(json);
- assertNotNull(config);
-
- // Entry should be skipped due to invalid regex
- assertTrue(config.getEntries().isEmpty());
- }
-
- @Test
- public void testUserDataPatternEmptyString() {
- String json = "{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": 1.0,\n" +
- " \"threadNameRegex\": \".*\",\n" +
- " \"userDataRegex\": \"\"\n" +
- " }\n" +
- " ]\n" +
- "}";
-
- SessionSaveDelayerConfig config =
SessionSaveDelayerConfig.fromJson(json);
- assertNotNull(config);
-
- List<SessionSaveDelayerConfig.DelayEntry> entries =
config.getEntries();
- assertEquals(1, entries.size());
-
- SessionSaveDelayerConfig.DelayEntry entry = entries.get(0);
-
- // Empty pattern should match empty string but not non-empty strings
- assertTrue(entry.matches("any-thread", "", null));
- assertTrue(entry.matches("any-thread", "anything", null)); // empty
regex matches anything using find()
- assertFalse(entry.matches("any-thread", null, null)); // null userData
should not match
- }
-
- @Test
- public void testUserDataPatternCaseInsensitive() {
- String json = "{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": 1.0,\n" +
- " \"threadNameRegex\": \".*\",\n" +
- " \"userDataRegex\": \"(?i)ADMIN.*\"\n" +
- " }\n" +
- " ]\n" +
- "}";
-
- SessionSaveDelayerConfig config =
SessionSaveDelayerConfig.fromJson(json);
- assertNotNull(config);
-
- List<SessionSaveDelayerConfig.DelayEntry> entries =
config.getEntries();
- assertEquals(1, entries.size());
-
- SessionSaveDelayerConfig.DelayEntry entry = entries.get(0);
-
- // Test case-insensitive matching
- assertTrue(entry.matches("any-thread", "admin", null));
- assertTrue(entry.matches("any-thread", "ADMIN", null));
- assertTrue(entry.matches("any-thread", "Admin123", null));
- assertTrue(entry.matches("any-thread", "administrator", null));
- assertFalse(entry.matches("any-thread", "user", null));
- }
-
- @Test
- public void testGetDelayNanosWithUserDataPattern() {
- String json = "{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": 1.0,\n" +
- " \"threadNameRegex\": \".*\",\n" +
- " \"userDataRegex\": \"admin.*\"\n" +
- " },\n" +
- " {\n" +
- " \"delayMillis\": 0.5,\n" +
- " \"threadNameRegex\": \".*\"\n" +
- " }\n" +
- " ]\n" +
- "}";
-
- SessionSaveDelayerConfig config =
SessionSaveDelayerConfig.fromJson(json);
-
- // When userData matches first entry's pattern
- assertEquals(1_000_000L, config.getDelayNanos("thread-1", "admin",
null));
- assertEquals(1_000_000L, config.getDelayNanos("thread-1", "admin123",
null));
-
- // When userData doesn't match first entry but matches second (no
userData pattern)
- assertEquals(500_000L, config.getDelayNanos("thread-1", "user", null));
- assertEquals(500_000L, config.getDelayNanos("thread-1", "guest",
null));
-
- // When userData is null, first entry shouldn't match but second should
- assertEquals(500_000L, config.getDelayNanos("thread-1", null, null));
- }
-
- @Test
- public void testRateLimitingBasic() {
- String json = "{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": 0.1,\n" +
- " \"threadNameRegex\": \".*\",\n" +
- " \"maxSavesPerSecond\": 2.0\n" +
- " }\n" +
- " ]\n" +
- "}";
-
- SessionSaveDelayerConfig config =
SessionSaveDelayerConfig.fromJson(json);
- assertNotNull(config);
-
- List<SessionSaveDelayerConfig.DelayEntry> entries =
config.getEntries();
- assertEquals(1, entries.size());
-
- SessionSaveDelayerConfig.DelayEntry entry = entries.get(0);
- assertEquals(100_000L, entry.getBaseDelayNanos());
- assertEquals(2.0, entry.getMaxSavesPerSecond(), 0.001);
-
- // First call should only have base delay
- long firstDelay = entry.getDelayNanos();
- assertEquals(100_000L, firstDelay);
-
- // Second call immediately should have additional rate limit delay
- // With 2 saves per second, minimum interval is 500ms
- long secondDelay = entry.getDelayNanos();
- assertTrue("Second delay should be >= base delay + rate limit delay",
- secondDelay >= 100_000L + 400_000_000L); // ~500ms in nanos
- }
-
- @Test
- public void testRateLimitingWithZeroMaxSaves() {
- String json = "{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": 0.1,\n" +
- " \"threadNameRegex\": \".*\",\n" +
- " \"maxSavesPerSecond\": 0.0\n" +
- " }\n" +
- " ]\n" +
- "}";
-
- SessionSaveDelayerConfig config =
SessionSaveDelayerConfig.fromJson(json);
- assertNotNull(config);
-
- List<SessionSaveDelayerConfig.DelayEntry> entries =
config.getEntries();
- assertEquals(1, entries.size());
-
- SessionSaveDelayerConfig.DelayEntry entry = entries.get(0);
- assertEquals(0.0, entry.getMaxSavesPerSecond(), 0.001);
-
- // All calls should only have base delay since rate limiting is
disabled
- assertEquals(100_000L, entry.getDelayNanos());
- assertEquals(100_000L, entry.getDelayNanos());
- assertEquals(100_000L, entry.getDelayNanos());
- }
-
- @Test
- public void testRateLimitingWithNegativeMaxSaves() {
- String json = "{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": 0.1,\n" +
- " \"threadNameRegex\": \".*\",\n" +
- " \"maxSavesPerSecond\": -1.0\n" +
- " }\n" +
- " ]\n" +
- "}";
-
- SessionSaveDelayerConfig config =
SessionSaveDelayerConfig.fromJson(json);
- assertNotNull(config);
-
- // Entry should be skipped due to negative maxSavesPerSecond
- assertTrue(config.getEntries().isEmpty());
- }
-
- @Test
- public void testRateLimitingWithInvalidMaxSaves() {
- String json = "{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": 0.1,\n" +
- " \"threadNameRegex\": \".*\",\n" +
- " \"maxSavesPerSecond\": \"invalid\"\n" +
- " }\n" +
- " ]\n" +
- "}";
-
- SessionSaveDelayerConfig config =
SessionSaveDelayerConfig.fromJson(json);
- assertNotNull(config);
-
- // Entry should be skipped due to invalid maxSavesPerSecond
- assertTrue(config.getEntries().isEmpty());
- }
-
- @Test
- public void testRateLimitingHighFrequency() {
- String json = "{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": 0.0,\n" +
- " \"threadNameRegex\": \".*\",\n" +
- " \"maxSavesPerSecond\": 10.0\n" +
- " }\n" +
- " ]\n" +
- "}";
-
- SessionSaveDelayerConfig config =
SessionSaveDelayerConfig.fromJson(json);
- SessionSaveDelayerConfig.DelayEntry entry = config.getEntries().get(0);
-
- // With 10 saves per second, minimum interval is 100ms
- long firstDelay = entry.getDelayNanos();
- assertEquals(0L, firstDelay); // No base delay
-
- long secondDelay = entry.getDelayNanos();
- assertTrue("Should have rate limit delay", secondDelay >=
90_000_000L); // ~100ms in nanos
- }
-
-
-
- @Test
- public void testRateLimitingCombinedWithUserDataPattern() {
- String json = "{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": 0.1,\n" +
- " \"threadNameRegex\": \".*\",\n" +
- " \"userDataRegex\": \"admin.*\",\n" +
- " \"maxSavesPerSecond\": 1.0\n" +
- " }\n" +
- " ]\n" +
- "}";
-
- SessionSaveDelayerConfig config =
SessionSaveDelayerConfig.fromJson(json);
- SessionSaveDelayerConfig.DelayEntry entry = config.getEntries().get(0);
-
- // Test rate limiting only applies when entry matches
- assertTrue(entry.matches("thread-1", "admin", null));
- assertFalse(entry.matches("thread-1", "user", null));
-
- // Rate limiting should work for matching entries
- long firstDelay = entry.getDelayNanos();
- assertEquals(100_000L, firstDelay);
-
- long secondDelay = entry.getDelayNanos();
- assertTrue("Should have rate limit delay", secondDelay >=
1_000_000_000L); // ~1000ms
- }
-
- @Test
- public void testRateLimitingJsonSerialization() {
- String json = "{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": 0.5,\n" +
- " \"threadNameRegex\": \"worker-.*\",\n" +
- " \"maxSavesPerSecond\": 3.5\n" +
- " }\n" +
- " ]\n" +
- "}";
-
- SessionSaveDelayerConfig config =
SessionSaveDelayerConfig.fromJson(json);
- String serialized = config.toString();
-
- // Check that serialization includes maxSavesPerSecond
- assertTrue(serialized.contains("\"maxSavesPerSecond\": 3.5"));
- assertTrue(serialized.contains("\"delayMillis\": 0.5"));
- assertTrue(serialized.contains("\"threadNameRegex\": \"worker-.*\""));
- }
-
- @Test
- public void testRateLimitingJsonSerializationWithoutMaxSaves() {
- String json = "{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": 0.5,\n" +
- " \"threadNameRegex\": \"worker-.*\"\n" +
- " }\n" +
- " ]\n" +
- "}";
-
- SessionSaveDelayerConfig config =
SessionSaveDelayerConfig.fromJson(json);
- String serialized = config.toString();
-
- // Check that serialization doesn't include maxSavesPerSecond when
it's 0
- assertFalse(serialized.contains("maxSavesPerSecond"));
- }
-
- @Test
- public void testGetDelayNanosWithRateLimit() {
- String json = "{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": 0.1,\n" +
- " \"threadNameRegex\": \".*\",\n" +
- " \"maxSavesPerSecond\": 2.0\n" +
- " }\n" +
- " ]\n" +
- "}";
-
- SessionSaveDelayerConfig config =
SessionSaveDelayerConfig.fromJson(json);
-
- // First call should only have base delay
- long firstDelay = config.getDelayNanos("thread-1", null, null);
- assertEquals(100_000L, firstDelay);
-
- // Second call should have additional rate limit delay
- long secondDelay = config.getDelayNanos("thread-1", null, null);
- assertTrue("Should have rate limit delay", secondDelay >=
400_000_000L);
- }
-}
\ No newline at end of file
diff --git
a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/session/SessionSaveDelayerTest.java
b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/session/SessionSaveDelayerTest.java
deleted file mode 100644
index f21001f07e..0000000000
---
a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/session/SessionSaveDelayerTest.java
+++ /dev/null
@@ -1,401 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.jackrabbit.oak.jcr.session;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import java.util.Map;
-
-import org.apache.jackrabbit.oak.api.jmx.RepositoryManagementMBean;
-import org.apache.jackrabbit.oak.commons.junit.TemporarySystemProperty;
-import org.apache.jackrabbit.oak.spi.whiteboard.DefaultWhiteboard;
-import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-
-/**
- * Tests for {@link SessionSaveDelayer}.
- */
-public class SessionSaveDelayerTest {
-
- private static final String ENABLED_PROP_NAME = "oak.sessionSaveDelayer";
- private static final String CONFIG_PROP_NAME =
"oak.sessionSaveDelayerConfig";
-
- @Rule
- public TemporarySystemProperty temp;
-
- private Whiteboard whiteboard;
- private SessionSaveDelayer delayer;
-
- @Before
- public void setUp() {
- whiteboard = new DefaultWhiteboard();
- delayer = new SessionSaveDelayer(whiteboard);
- }
-
- @After
- public void tearDown() {
- if (delayer != null) {
- delayer.close();
- }
- }
-
- @Test
- public void testGetCurrentStackTrace() {
- String stackTrace = SessionSaveDelayerConfig.getCurrentStackTrace();
- assertNotNull(stackTrace);
- assertTrue(stackTrace.contains("testGetCurrentStackTrace"));
- assertTrue(stackTrace.contains("at "));
- }
-
- @Test
- public void testDelayIfNeededDisabled() {
- System.clearProperty(ENABLED_PROP_NAME);
- delayer = new SessionSaveDelayer(whiteboard);
- long delay = delayer.delayIfNeeded(null);
- assertEquals(0, delay);
- }
-
- @Test
- public void testDelayIfNeededEnabledViaSystemProperty() {
- System.setProperty(ENABLED_PROP_NAME, "true");
- System.clearProperty(CONFIG_PROP_NAME);
- delayer = new SessionSaveDelayer(whiteboard);
- long delay = delayer.delayIfNeeded(null);
- assertEquals(0, delay);
- }
-
- @Test
- public void testDelayIfNeededWithSystemPropertyConfig() {
- System.setProperty(ENABLED_PROP_NAME, "true");
- System.setProperty(CONFIG_PROP_NAME, "{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": 0.1,\n" +
- " \"threadNameRegex\": \".*\"\n" +
- " }\n" +
- " ]\n" +
- "}");
- delayer = new SessionSaveDelayer(whiteboard);
- long delay = delayer.delayIfNeeded(null);
- assertEquals(100_000L, delay);
- }
-
- @Test
- public void testDelayIfNeededWithJMXConfig() {
- System.setProperty(ENABLED_PROP_NAME, "true");
- RepositoryManagementMBean mbean =
mock(RepositoryManagementMBean.class);
- when(mbean.getSessionSaveDelayerConfig()).thenReturn("{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": 0.02,\n" +
- " \"threadNameRegex\": \".*\"\n" +
- " }\n" +
- " ]\n" +
- "}");
- whiteboard.register(RepositoryManagementMBean.class, mbean, Map.of());
- delayer = new SessionSaveDelayer(whiteboard);
- long delay = delayer.delayIfNeeded(null);
- assertEquals(20_000L, delay);
- }
-
- @Test
- public void testDelayIfNeededWithNonMatchingThreadName() {
- System.setProperty(ENABLED_PROP_NAME, "true");
- System.setProperty(CONFIG_PROP_NAME, "{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": 5.0,\n" +
- " \"threadNameRegex\": \"non-matching-pattern\"\n" +
- " }\n" +
- " ]\n" +
- "}");
- delayer = new SessionSaveDelayer(whiteboard);
- long delay = delayer.delayIfNeeded(null);
- assertEquals(0, delay);
- }
-
- @Test
- public void testDelayIfNeededWithInvalidConfig() {
- System.setProperty(ENABLED_PROP_NAME, "true");
- System.setProperty(CONFIG_PROP_NAME, "{ invalid json }");
- delayer = new SessionSaveDelayer(whiteboard);
- long delay = delayer.delayIfNeeded(null);
- assertEquals(0, delay);
- }
-
- @Test
- public void testDelayIfNeededConfigCaching() {
- System.setProperty(ENABLED_PROP_NAME, "true");
- System.setProperty(CONFIG_PROP_NAME, "{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": 0.1,\n" +
- " \"threadNameRegex\": \".*\"\n" +
- " }\n" +
- " ]\n" +
- "}");
- delayer = new SessionSaveDelayer(whiteboard);
- long delay1 = delayer.delayIfNeeded(null);
- assertEquals(100_000L, delay1);
- long delay2 = delayer.delayIfNeeded(null);
- assertEquals(100_000L, delay2);
- }
-
- @Test
- public void testDelayIfNeededEmptyConfig() {
- System.setProperty(ENABLED_PROP_NAME, "true");
- System.setProperty(CONFIG_PROP_NAME, "");
- delayer = new SessionSaveDelayer(whiteboard);
- long delay = delayer.delayIfNeeded(null);
- assertEquals(0, delay);
- }
-
- @Test
- public void testDelayIfNeededAfterClose() {
- long delay = delayer.delayIfNeeded(null);
- assertEquals(0, delay);
- }
-
- @Test
- public void testJMXConfigOverridesSystemProperty() {
- System.setProperty(ENABLED_PROP_NAME, "true");
- System.setProperty(CONFIG_PROP_NAME, "{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": 0.1,\n" +
- " \"threadNameRegex\": \".*\"\n" +
- " }\n" +
- " ]\n" +
- "}");
- RepositoryManagementMBean mbean =
mock(RepositoryManagementMBean.class);
- when(mbean.getSessionSaveDelayerConfig()).thenReturn("{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": 0.3,\n" +
- " \"threadNameRegex\": \".*\"\n" +
- " }\n" +
- " ]\n" +
- "}");
-
- whiteboard.register(RepositoryManagementMBean.class, mbean, Map.of());
- delayer = new SessionSaveDelayer(whiteboard);
- long delay = delayer.delayIfNeeded(null);
- assertEquals(300_000L, delay);
- }
-
- @Test
- public void testDelayIfNeededWithMultipleEntries() {
- System.setProperty(ENABLED_PROP_NAME, "true");
- System.setProperty(CONFIG_PROP_NAME, "{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": 0.1,\n" +
- " \"threadNameRegex\": \"non-matching\"\n" +
- " },\n" +
- " {\n" +
- " \"delayMillis\": 0.2,\n" +
- " \"threadNameRegex\": \".*\"\n" +
- " }\n" +
- " ]\n" +
- "}");
-
- delayer = new SessionSaveDelayer(whiteboard);
- long delay = delayer.delayIfNeeded(null);
- assertEquals(200_000L, delay);
- }
-
- @Test
- public void testDelayIfNeededWithUserDataPattern() {
- System.setProperty(ENABLED_PROP_NAME, "true");
- System.setProperty(CONFIG_PROP_NAME, "{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": 0.1,\n" +
- " \"threadNameRegex\": \".*\",\n" +
- " \"userDataRegex\": \"admin.*\"\n" +
- " }\n" +
- " ]\n" +
- "}");
-
- delayer = new SessionSaveDelayer(whiteboard);
-
- // Test with matching userData
- long delay = delayer.delayIfNeeded("admin");
- assertEquals(100_000L, delay);
-
- delay = delayer.delayIfNeeded("admin123");
- assertEquals(100_000L, delay);
-
- // Test with non-matching userData
- delay = delayer.delayIfNeeded("user");
- assertEquals(0L, delay);
-
- // Test with null userData
- delay = delayer.delayIfNeeded(null);
- assertEquals(0L, delay);
- }
-
- @Test
- public void testDelayIfNeededWithUserDataPatternComplexRegex() {
- System.setProperty(ENABLED_PROP_NAME, "true");
- System.setProperty(CONFIG_PROP_NAME, "{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": 0.2,\n" +
- " \"threadNameRegex\": \".*\",\n" +
- " \"userDataRegex\":
\"(admin|root|system)@.*\\\\.com$\"\n" +
- " }\n" +
- " ]\n" +
- "}");
-
- delayer = new SessionSaveDelayer(whiteboard);
-
- // Test with matching email patterns
- long delay = delayer.delayIfNeeded("[email protected]");
- assertEquals(200_000L, delay);
-
- delay = delayer.delayIfNeeded("[email protected]");
- assertEquals(200_000L, delay);
-
- delay = delayer.delayIfNeeded("[email protected]");
- assertEquals(200_000L, delay);
-
- // Test with non-matching patterns
- delay = delayer.delayIfNeeded("[email protected]");
- assertEquals(0L, delay);
-
- delay = delayer.delayIfNeeded("[email protected]");
- assertEquals(0L, delay);
-
- delay = delayer.delayIfNeeded("admin");
- assertEquals(0L, delay);
- }
-
- @Test
- public void testDelayIfNeededWithUserDataPatternMultipleEntries() {
- System.setProperty(ENABLED_PROP_NAME, "true");
- System.setProperty(CONFIG_PROP_NAME, "{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": 0.1,\n" +
- " \"threadNameRegex\": \".*\",\n" +
- " \"userDataRegex\": \"admin.*\"\n" +
- " },\n" +
- " {\n" +
- " \"delayMillis\": 0.2,\n" +
- " \"threadNameRegex\": \".*\",\n" +
- " \"userDataRegex\": \"guest.*\"\n" +
- " },\n" +
- " {\n" +
- " \"delayMillis\": 0.3,\n" +
- " \"threadNameRegex\": \".*\"\n" +
- " }\n" +
- " ]\n" +
- "}");
-
- delayer = new SessionSaveDelayer(whiteboard);
-
- // Test first entry matches
- long delay = delayer.delayIfNeeded("admin");
- assertEquals(100_000L, delay);
-
- delay = delayer.delayIfNeeded("admin123");
- assertEquals(100_000L, delay);
-
- // Test second entry matches
- delay = delayer.delayIfNeeded("guest123");
- assertEquals(200_000L, delay);
-
- // Test third entry matches (no userData pattern)
- delay = delayer.delayIfNeeded("normalUser");
- assertEquals(300_000L, delay);
-
- // Test with null userData - should match third entry
- delay = delayer.delayIfNeeded(null);
- assertEquals(300_000L, delay);
- }
-
- @Test
- public void testDelayIfNeededWithUserDataPatternAndJMX() {
- System.setProperty(ENABLED_PROP_NAME, "true");
-
- RepositoryManagementMBean mbean =
mock(RepositoryManagementMBean.class);
- when(mbean.getSessionSaveDelayerConfig()).thenReturn("{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": 0.15,\n" +
- " \"threadNameRegex\": \".*\",\n" +
- " \"userDataRegex\": \"privileged.*\"\n" +
- " }\n" +
- " ]\n" +
- "}");
-
- whiteboard.register(RepositoryManagementMBean.class, mbean, Map.of());
- delayer = new SessionSaveDelayer(whiteboard);
-
- // Test with matching userData
- long delay = delayer.delayIfNeeded("privilegedUser");
- assertEquals(150_000L, delay);
-
- // Test with non-matching userData
- delay = delayer.delayIfNeeded("normalUser");
- assertEquals(0L, delay);
- }
-
- @Test
- public void testDelayIfNeededWithUserDataPatternCaseInsensitive() {
- System.setProperty(ENABLED_PROP_NAME, "true");
- System.setProperty(CONFIG_PROP_NAME, "{\n" +
- " \"entries\": [\n" +
- " {\n" +
- " \"delayMillis\": 0.1,\n" +
- " \"threadNameRegex\": \".*\",\n" +
- " \"userDataRegex\": \"(?i)ADMIN.*\"\n" +
- " }\n" +
- " ]\n" +
- "}");
-
- delayer = new SessionSaveDelayer(whiteboard);
-
- // Test case-insensitive matching
- long delay = delayer.delayIfNeeded("admin");
- assertEquals(100_000L, delay);
-
- delay = delayer.delayIfNeeded("ADMIN");
- assertEquals(100_000L, delay);
-
- delay = delayer.delayIfNeeded("Admin123");
- assertEquals(100_000L, delay);
-
- delay = delayer.delayIfNeeded("administrator");
- assertEquals(100_000L, delay);
-
- // Test non-matching
- delay = delayer.delayIfNeeded("user");
- assertEquals(0L, delay);
- }
-
-
-
-}
\ No newline at end of file