/
AbstractSessionDataStore.java
375 lines (328 loc) · 12.8 KB
/
AbstractSessionDataStore.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
//
// ========================================================================
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.server.session;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.util.FuturePromise;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* AbstractSessionDataStore
*/
@ManagedObject
public abstract class AbstractSessionDataStore extends ContainerLifeCycle implements SessionDataStore
{
private static final Logger LOG = LoggerFactory.getLogger(AbstractSessionDataStore.class);
public static final int DEFAULT_GRACE_PERIOD_SEC = 60 * 60; //default of 1hr
public static final int DEFAULT_SAVE_PERIOD_SEC = 0;
protected SessionContext _context; //context associated with this session data store
protected int _gracePeriodSec = DEFAULT_GRACE_PERIOD_SEC;
protected long _lastExpiryCheckTime = 0; //last time in ms that getExpired was called
protected long _lastOrphanSweepTime = 0; //last time in ms that we deleted orphaned sessions
protected int _savePeriodSec = DEFAULT_SAVE_PERIOD_SEC; //time in sec between saves
/**
* Check if a session for the given id exists.
*
* @param id the session id to check
* @return true if the session exists in the persistent store, false otherwise
*/
public abstract boolean doExists(String id) throws Exception;
/**
* Store the session data persistently.
*
* @param id identity of session to store
* @param data info of the session
* @param lastSaveTime time of previous save or 0 if never saved
* @throws Exception if unable to store data
*/
public abstract void doStore(String id, SessionData data, long lastSaveTime) throws Exception;
/**
* Load the session from persistent store.
*
* @param id the id of the session to load
* @return the re-inflated session
* @throws Exception if unable to load the session
*/
public abstract SessionData doLoad(String id) throws Exception;
/**
* Implemented by subclasses to resolve which sessions in this context
* that are being managed by this node that should be expired.
*
* @param candidates the ids of sessions the SessionCache thinks has expired
* @param time the time at which to check for expiry
* @return the reconciled set of session ids that have been checked in the store
*/
public abstract Set<String> doCheckExpired(Set<String> candidates, long time);
/**
* Implemented by subclasses to find sessions for this context in the store
* that expired at or before the time limit and thus not being actively
* managed by any node. This method is only called periodically (the period
* is configurable) to avoid putting too much load on the store.
*
* @param before the upper limit of expiry times to check. Sessions expired
* at or before this timestamp will match.
*
* @return the empty set if there are no sessions expired as at the time, or
* otherwise a set of session ids.
*/
public abstract Set<String> doGetExpired(long before);
/**
* Implemented by subclasses to delete sessions for other contexts that
* expired at or before the timeLimit. These are 'orphaned' sessions that
* are no longer being actively managed by any node. These are explicitly
* sessions that do NOT belong to this context (other mechanisms such as
* doGetExpired take care of those). As they don't belong to this context,
* they cannot be loaded by us.
*
* This is called only periodically to avoid placing excessive load on the
* store.
*
* @param time the upper limit of the expiry time to check in msec
*/
public abstract void doCleanOrphans(long time);
@Override
public void initialize(SessionContext context) throws Exception
{
if (isStarted())
throw new IllegalStateException("Context set after SessionDataStore started");
_context = context;
}
/**
* Remove all sessions for any context that expired at or before the given time.
* @param timeLimit the time before which the sessions must have expired.
*/
public void cleanOrphans(long timeLimit)
{
if (!isStarted())
throw new IllegalStateException("Not started");
Runnable r = () ->
{
doCleanOrphans(timeLimit);
};
_context.run(r);
}
@Override
public SessionData load(String id) throws Exception
{
if (!isStarted())
throw new IllegalStateException("Not started");
final FuturePromise<SessionData> result = new FuturePromise<>();
Runnable r = () ->
{
try
{
result.succeeded(doLoad(id));
}
catch (Exception e)
{
result.failed(e);
}
};
_context.run(r);
return result.getOrThrow();
}
@Override
public void store(String id, SessionData data) throws Exception
{
if (!isStarted())
throw new IllegalStateException("Not started");
if (data == null)
return;
long lastSave = data.getLastSaved();
long savePeriodMs = (_savePeriodSec <= 0 ? 0 : TimeUnit.SECONDS.toMillis(_savePeriodSec));
if (LOG.isDebugEnabled())
{
LOG.debug("Store: id={}, mdirty={}, dirty={}, lsave={}, period={}, elapsed={}", id, data.isMetaDataDirty(),
data.isDirty(), data.getLastSaved(), savePeriodMs, (System.currentTimeMillis() - lastSave));
}
//save session if attribute changed, never been saved or metadata changed (eg expiry time) and save interval exceeded
if (data.isDirty() || (lastSave <= 0) ||
(data.isMetaDataDirty() && ((System.currentTimeMillis() - lastSave) >= savePeriodMs)))
{
//set the last saved time to now
data.setLastSaved(System.currentTimeMillis());
final FuturePromise<Void> result = new FuturePromise<>();
Runnable r = () ->
{
try
{
//call the specific store method, passing in previous save time
doStore(id, data, lastSave);
data.clean(); //unset all dirty flags
result.succeeded(null);
}
catch (Exception e)
{
//reset last save time if save failed
data.setLastSaved(lastSave);
result.failed(e);
}
};
_context.run(r);
result.getOrThrow();
}
}
@Override
public boolean exists(String id) throws Exception
{
FuturePromise<Boolean> result = new FuturePromise<>();
Runnable r = () ->
{
try
{
result.succeeded(doExists(id));
}
catch (Exception e)
{
result.failed(e);
}
};
_context.run(r);
return result.getOrThrow();
}
@Override
public Set<String> getExpired(Set<String> candidates)
{
if (!isStarted())
throw new IllegalStateException("Not started");
long now = System.currentTimeMillis();
final Set<String> expired = new HashSet<>();
// 1. always verify the set of candidates we've been given
//by the sessioncache
Runnable r = () ->
{
Set<String> expiredCandidates = doCheckExpired(candidates, now);
if (expiredCandidates != null)
expired.addAll(expiredCandidates);
};
_context.run(r);
// 2. check the backing store to find other sessions
// in THIS context that expired long ago (ie cannot be actively managed
//by any node)
try
{
long t = 0;
// if we have never checked for old expired sessions, then only find
// those that are very old so we don't find sessions that other nodes
// that are also starting up find
if (_lastExpiryCheckTime <= 0)
t = now - TimeUnit.SECONDS.toMillis(_gracePeriodSec * 3);
else
{
// only do the check once every gracePeriod to avoid expensive searches,
// and find sessions that expired at least one gracePeriod ago
if (now > (_lastExpiryCheckTime + TimeUnit.SECONDS.toMillis(_gracePeriodSec)))
t = now - TimeUnit.SECONDS.toMillis(_gracePeriodSec);
}
if (t > 0)
{
if (LOG.isDebugEnabled())
LOG.debug("Searching for sessions expired before {} for context {}", t, _context.getCanonicalContextPath());
final long expiryTime = t;
r = () ->
{
Set<String> tmp = doGetExpired(expiryTime);
if (tmp != null)
expired.addAll(tmp);
};
_context.run(r);
}
}
finally
{
_lastExpiryCheckTime = now;
}
// 3. Periodically but infrequently comb the backing store to delete sessions for
// OTHER contexts that expired a very long time ago (ie not being actively
// managed by any node). As these sessions are not for our context, we
// can't load them, so they must just be forcibly deleted.
try
{
if (now > (_lastOrphanSweepTime + TimeUnit.SECONDS.toMillis(10 * _gracePeriodSec)))
{
if (LOG.isDebugEnabled())
LOG.debug("Cleaning orphans at {}, last sweep at {}", now, _lastOrphanSweepTime);
cleanOrphans(now - TimeUnit.SECONDS.toMillis(10 * _gracePeriodSec));
}
}
finally
{
_lastOrphanSweepTime = now;
}
return expired;
}
@Override
public SessionData newSessionData(String id, long created, long accessed, long lastAccessed, long maxInactiveMs)
{
return new SessionData(id, _context.getCanonicalContextPath(), _context.getVhost(), created, accessed, lastAccessed, maxInactiveMs);
}
protected void checkStarted() throws IllegalStateException
{
if (isStarted())
throw new IllegalStateException("Already started");
}
@Override
protected void doStart() throws Exception
{
if (_context == null)
throw new IllegalStateException("No SessionContext");
super.doStart();
}
@ManagedAttribute(value = "interval in secs to prevent too eager session scavenging", readonly = true)
public int getGracePeriodSec()
{
return _gracePeriodSec;
}
public void setGracePeriodSec(int sec)
{
_gracePeriodSec = sec;
}
/**
* @return the savePeriodSec
*/
@ManagedAttribute(value = "min secs between saves", readonly = true)
public int getSavePeriodSec()
{
return _savePeriodSec;
}
/**
* The minimum time in seconds between save operations.
* Saves normally occur every time the last request
* exits as session. If nothing changes on the session
* except for the access time and the persistence technology
* is slow, this can cause delays.
* <p>
* By default the value is 0, which means we save
* after the last request exists. A non zero value
* means that we will skip doing the save if the
* session isn't dirty if the elapsed time since
* the session was last saved does not exceed this
* value.
*
* @param savePeriodSec the savePeriodSec to set
*/
public void setSavePeriodSec(int savePeriodSec)
{
_savePeriodSec = savePeriodSec;
}
@Override
public String toString()
{
return String.format("%s@%x[passivating=%b,graceSec=%d]", this.getClass().getName(), this.hashCode(), isPassivating(), getGracePeriodSec());
}
}