Skip to content

Commit c736595

Browse files
committed
OAK-11766 Write Throttling Mechanism - Session.save() delay
1 parent 19ffddb commit c736595

File tree

5 files changed

+713
-65
lines changed

5 files changed

+713
-65
lines changed

oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/SessionDelegate.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ private void commit(Root root, String path) throws CommitFailedException {
398398
if (userData != null) {
399399
info.put(EventFactory.USER_DATA, userData);
400400
}
401-
sessionSaveDelayer.delayIfNeeded();
401+
sessionSaveDelayer.delayIfNeeded(userData);
402402
root.commit(Collections.unmodifiableMap(info));
403403
if (permissionProvider != null && refreshPermissionProvider) {
404404
permissionProvider.refresh();

oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/session/SessionSaveDelayer.java

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -68,24 +68,14 @@ public SessionSaveDelayer(@NotNull Whiteboard whiteboard) {
6868
this.whiteboard = whiteboard;
6969
}
7070

71-
/**
72-
* Gets the name of the current thread.
73-
*
74-
* @return the current thread name
75-
*/
76-
@NotNull
77-
public static String getCurrentThreadName() {
78-
return Thread.currentThread().getName();
79-
}
80-
8171
private RepositoryManagementMBean getRepositoryMBean() {
8272
if (cachedMbean == null) {
8373
cachedMbean = WhiteboardUtils.getService(whiteboard, RepositoryManagementMBean.class);
8474
}
8575
return cachedMbean;
8676
}
8777

88-
public long delayIfNeeded() {
78+
public long delayIfNeeded(String userData) {
8979
if (closed.get() || (!feature.isEnabled() && !enabledViaSysPropertey)) {
9080
return 0;
9181
}
@@ -118,12 +108,12 @@ public long delayIfNeeded() {
118108
return 0;
119109
}
120110
String threadName = Thread.currentThread().getName();
121-
long delayNanos = lastConfig.getDelayNanos(threadName, null);
111+
long delayNanos = lastConfig.getDelayNanos(threadName, userData, null);
122112
if (delayNanos > 0) {
123113
long millis = delayNanos / 1_000_000;
124114
int nanos = (int) (delayNanos % 1_000_000);
125115
if (logNextDelay) {
126-
LOG.info("Sleep {} ms {} ns", millis, nanos);
116+
LOG.info("Sleep {} ms {} ns for user {}", millis, nanos, userData);
127117
logNextDelay = false;
128118
}
129119
try {

oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/session/SessionSaveDelayerConfig.java

Lines changed: 72 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.util.ArrayList;
2020
import java.util.List;
21+
import java.util.concurrent.atomic.AtomicLong;
2122
import java.util.regex.Pattern;
2223
import java.util.regex.PatternSyntaxException;
2324

@@ -96,10 +97,10 @@ public List<DelayEntry> getEntries() {
9697
return entries;
9798
}
9899

99-
public long getDelayNanos(@NotNull String threadName, @Nullable String stackTrace) {
100+
public long getDelayNanos(@NotNull String threadName, @Nullable String userData, @Nullable String stackTrace) {
100101
for (DelayEntry d : entries) {
101-
if (d.matches(threadName, stackTrace)) {
102-
return d.delayNanos;
102+
if (d.matches(threadName, userData, stackTrace)) {
103+
return d.getDelayNanos();
103104
}
104105
}
105106
return 0;
@@ -109,7 +110,9 @@ public long getDelayNanos(@NotNull String threadName, @Nullable String stackTrac
109110
private static DelayEntry parseDelayEntry(JsonObject entryObj) {
110111
String delayMillis = entryObj.getProperties().get("delayMillis");
111112
String threadNameRegex = entryObj.getProperties().get("threadNameRegex");
113+
String userDataRegex = entryObj.getProperties().get("userDataRegex");
112114
String stackTraceRegex = entryObj.getProperties().get("stackTraceRegex");
115+
String maxSavesPerSecond = entryObj.getProperties().get("maxSavesPerSecond");
113116
if (delayMillis == null || threadNameRegex == null) {
114117
LOG.warn("Skipping entry with missing required fields (delay or threadNameRegex)");
115118
return null;
@@ -120,14 +123,26 @@ private static DelayEntry parseDelayEntry(JsonObject entryObj) {
120123
LOG.warn("Skipping entry with negative delay");
121124
return null;
122125
}
126+
double maxSaves = 0.0;
127+
if (maxSavesPerSecond != null) {
128+
maxSaves = Double.parseDouble(maxSavesPerSecond);
129+
if (maxSaves < 0) {
130+
LOG.warn("Skipping entry with negative maxSavesPerSecond");
131+
return null;
132+
}
133+
}
123134
Pattern threadPattern = Pattern.compile(JsopTokenizer.decodeQuoted(threadNameRegex));
124135
Pattern stackPattern = null;
125136
if (stackTraceRegex != null) {
126137
stackPattern = Pattern.compile(JsopTokenizer.decodeQuoted(stackTraceRegex));
127138
}
128-
return new DelayEntry(delay, threadPattern, stackPattern);
139+
Pattern userDataPattern = null;
140+
if (userDataRegex != null) {
141+
userDataPattern = Pattern.compile(JsopTokenizer.decodeQuoted(userDataRegex));
142+
}
143+
return new DelayEntry(delay, threadPattern, userDataPattern, stackPattern, maxSaves);
129144
} catch (NumberFormatException e) {
130-
LOG.warn("Skipping entry with invalid delay value: {}", delayMillis);
145+
LOG.warn("Skipping entry with invalid delay value or maxSavesPerSecond: {}", e.getMessage());
131146
return null;
132147
} catch (PatternSyntaxException e) {
133148
LOG.warn("Skipping entry with invalid regex pattern: {}", e.getMessage());
@@ -170,18 +185,45 @@ public static String getCurrentStackTrace() {
170185
}
171186

172187
public static class DelayEntry {
173-
private final long delayNanos;
188+
private final long baseDelayNanos;
174189
private final Pattern threadNamePattern;
175190
private final Pattern stackTracePattern;
191+
private final Pattern userDataPattern;
192+
private final double maxSavesPerSecond;
193+
private final AtomicLong lastMatch = new AtomicLong(0);
176194

177-
public DelayEntry(double delayMillis, @NotNull Pattern threadNamePattern, @Nullable Pattern stackTracePattern) {
178-
this.delayNanos = (long) (delayMillis * 1_000_000);
195+
public DelayEntry(double delayMillis, @NotNull Pattern threadNamePattern, @Nullable Pattern userDataPattern, @Nullable Pattern stackTracePattern, double maxSavesPerSecond) {
196+
this.baseDelayNanos = (long) (delayMillis * 1_000_000);
179197
this.threadNamePattern = threadNamePattern;
198+
this.userDataPattern = userDataPattern;
180199
this.stackTracePattern = stackTracePattern;
200+
this.maxSavesPerSecond = maxSavesPerSecond;
181201
}
182202

183203
public long getDelayNanos() {
184-
return delayNanos;
204+
long totalDelayNanos = baseDelayNanos;
205+
if (maxSavesPerSecond > 0) {
206+
long currentTime = System.currentTimeMillis();
207+
double intervalMs = 1000.0 / maxSavesPerSecond;
208+
long lastMatchTime = lastMatch.get();
209+
if (lastMatchTime > 0) {
210+
long nextAllowedTime = lastMatchTime + (long) intervalMs;
211+
if (currentTime < nextAllowedTime) {
212+
long rateLimitDelayMs = nextAllowedTime - currentTime;
213+
totalDelayNanos += rateLimitDelayMs * 1_000_000;
214+
}
215+
}
216+
lastMatch.set(currentTime);
217+
}
218+
return totalDelayNanos;
219+
}
220+
221+
public long getBaseDelayNanos() {
222+
return baseDelayNanos;
223+
}
224+
225+
public double getMaxSavesPerSecond() {
226+
return maxSavesPerSecond;
185227
}
186228

187229
@NotNull
@@ -194,10 +236,23 @@ public Pattern getStackTracePattern() {
194236
return stackTracePattern;
195237
}
196238

197-
boolean matches(@NotNull String threadName, @Nullable String stackTrace) {
239+
@Nullable
240+
public Pattern getUserDataPattern() {
241+
return userDataPattern;
242+
}
243+
244+
boolean matches(@NotNull String threadName, @Nullable String userData, @Nullable String stackTrace) {
198245
if (!threadNamePattern.matcher(threadName).matches()) {
199246
return false;
200247
}
248+
if (userDataPattern != null) {
249+
if (userData == null) {
250+
return false;
251+
}
252+
if (!userDataPattern.matcher(userData).find()) {
253+
return false;
254+
}
255+
}
201256
if (stackTracePattern != null) {
202257
if (stackTrace == null) {
203258
stackTrace = SessionSaveDelayerConfig.getCurrentStackTrace();
@@ -214,12 +269,18 @@ public String toString() {
214269

215270
public JsopBuilder toJson(JsopBuilder json) {
216271
json.object();
217-
double delayMillis = delayNanos / 1_000_000.0;
272+
double delayMillis = baseDelayNanos / 1_000_000.0;
218273
json.key("delayMillis").encodedValue(Double.toString(delayMillis));
219274
json.key("threadNameRegex").value(threadNamePattern.pattern());
275+
if (userDataPattern != null) {
276+
json.key("userDataRegex").value(userDataPattern.pattern());
277+
}
220278
if (stackTracePattern != null) {
221279
json.key("stackTraceRegex").value(stackTracePattern.pattern());
222280
}
281+
if (maxSavesPerSecond > 0) {
282+
json.key("maxSavesPerSecond").encodedValue(Double.toString(maxSavesPerSecond));
283+
}
223284
return json.endObject();
224285
}
225286

0 commit comments

Comments
 (0)