Mercurial > dive4elements > framework
comparison artifact-database/src/main/java/org/dive4elements/artifactdatabase/DatabaseCleaner.java @ 473:d0ac790a6c89 dive4elements-move
Moved directories to org.dive4elements
author | Sascha L. Teichmann <teichmann@intevation.de> |
---|---|
date | Thu, 25 Apr 2013 10:57:18 +0200 |
parents | artifact-database/src/main/java/de/intevation/artifactdatabase/DatabaseCleaner.java@f367be55dd35 |
children | 415df0fc4fa1 |
comparison
equal
deleted
inserted
replaced
472:783cc1b6b615 | 473:d0ac790a6c89 |
---|---|
1 /* | |
2 * Copyright (c) 2010 by Intevation GmbH | |
3 * | |
4 * This program is free software under the LGPL (>=v2.1) | |
5 * Read the file LGPL.txt coming with the software for details | |
6 * or visit http://www.gnu.org/licenses/ if it does not exist. | |
7 */ | |
8 | |
9 package de.intevation.artifactdatabase; | |
10 | |
11 import de.intevation.artifacts.common.utils.Config; | |
12 import de.intevation.artifacts.common.utils.StringUtils; | |
13 | |
14 import de.intevation.artifacts.Artifact; | |
15 | |
16 import de.intevation.artifactdatabase.db.SQL; | |
17 import de.intevation.artifactdatabase.db.DBConnection; | |
18 | |
19 import java.sql.Connection; | |
20 import java.sql.PreparedStatement; | |
21 import java.sql.ResultSet; | |
22 import java.sql.SQLException; | |
23 | |
24 import java.util.ArrayList; | |
25 import java.util.List; | |
26 import java.util.Set; | |
27 import java.util.Collections; | |
28 | |
29 import javax.sql.DataSource; | |
30 | |
31 import org.apache.log4j.Logger; | |
32 | |
33 /** | |
34 * The database cleaner runs in background. It sleep for a configurable | |
35 * while and when it wakes up it removes outdated artifacts from the | |
36 * database. Outdated means that the the last access to the artifact | |
37 * is longer aga then the time to live of this artifact.<br> | |
38 * Before the artifact is finally removed from the system it is | |
39 * revived one last time an the #endOfLife() method of the artifact | |
40 * is called.<br> | |
41 * The artifact implementations may e.g. use this to remove some extrenal | |
42 * resources form the system. | |
43 * | |
44 * @author <a href="mailto:sascha.teichmann@intevation.de">Sascha L. Teichmann</a> | |
45 */ | |
46 public class DatabaseCleaner | |
47 extends Thread | |
48 { | |
49 /** | |
50 * Implementors of this interface are able to create a | |
51 * living artifact from a given byte array. | |
52 */ | |
53 public interface ArtifactReviver { | |
54 | |
55 /** | |
56 * Called to revive an artifact from a given byte array. | |
57 * @param factoryName The name of the factory which | |
58 * created this artifact. | |
59 * @param bytes The bytes of the serialized artifact. | |
60 * @return The revived artfiact. | |
61 */ | |
62 Artifact reviveArtifact(String factoryName, byte [] bytes); | |
63 | |
64 void killedArtifacts(List<String> identifiers); | |
65 void killedCollections(List<String> identifiers); | |
66 | |
67 } // interface ArtifactReviver | |
68 | |
69 public interface LockedIdsProvider { | |
70 Set<Integer> getLockedIds(); | |
71 } // interface LockedIdsProvider | |
72 | |
73 private static Logger logger = Logger.getLogger(DatabaseCleaner.class); | |
74 | |
75 /** | |
76 * Number of artifacts to be loaded at once. Used to | |
77 * mitigate the problem of a massive denial of service | |
78 * if too many artifacts have died since last cleanup. | |
79 */ | |
80 public static final int MAX_ROWS = 50; | |
81 | |
82 public static final Set<Integer> EMPTY_IDS = Collections.emptySet(); | |
83 | |
84 /** | |
85 * The SQL statement to select the outdated artifacts. | |
86 */ | |
87 public String SQL_OUTDATED; | |
88 | |
89 public String SQL_OUTDATED_COLLECTIONS; | |
90 public String SQL_DELETE_COLLECTION_ITEMS; | |
91 public String SQL_DELETE_COLLECTION; | |
92 | |
93 /** | |
94 * The SQL statement to delete some artifacts from the database. | |
95 */ | |
96 public String SQL_DELETE_ARTIFACT; | |
97 | |
98 /** | |
99 * XPath to figure out how long the cleaner should sleep between | |
100 * cleanups. This is stored in the global configuration. | |
101 */ | |
102 public static final String SLEEP_XPATH = | |
103 "/artifact-database/cleaner/sleep-time/text()"; | |
104 | |
105 /** | |
106 * Default nap time between cleanups: 5 minutes. | |
107 */ | |
108 public static final long SLEEP_DEFAULT = | |
109 5 * 60 * 1000L; // 5 minutes | |
110 | |
111 /** | |
112 * The configured nap time. | |
113 */ | |
114 protected long sleepTime; | |
115 | |
116 /** | |
117 * Internal locking mechanism to prevent some race conditions. | |
118 */ | |
119 protected Object sleepLock = new Object(); | |
120 | |
121 /** | |
122 * A reference to the global context. | |
123 */ | |
124 protected Object context; | |
125 | |
126 /** | |
127 * A specialized Id filter which only delete some artifacts. | |
128 * This is used to prevent deletion of living artifacts. | |
129 */ | |
130 protected LockedIdsProvider lockedIdsProvider; | |
131 | |
132 /** | |
133 * The reviver used to bring the dead artifact on last | |
134 * time back to live to call endOfLife() on them. | |
135 */ | |
136 protected ArtifactReviver reviver; | |
137 | |
138 protected DBConnection dbConnection; | |
139 | |
140 /** | |
141 * Default constructor. | |
142 */ | |
143 public DatabaseCleaner() { | |
144 } | |
145 | |
146 /** | |
147 * Constructor to create a cleaner with a given global context | |
148 * and a given reviver. | |
149 * @param context The global context of the artifact database | |
150 * @param reviver The reviver to awake artifact one last time. | |
151 */ | |
152 public DatabaseCleaner(Object context, ArtifactReviver reviver, DBConfig config) { | |
153 setDaemon(true); | |
154 sleepTime = getSleepTime(); | |
155 this.context = context; | |
156 this.reviver = reviver; | |
157 this.dbConnection = config.getDBConnection(); | |
158 setupSQL(config.getSQL()); | |
159 } | |
160 | |
161 protected void setupSQL(SQL sql) { | |
162 SQL_OUTDATED = sql.get("artifacts.outdated"); | |
163 SQL_OUTDATED_COLLECTIONS = sql.get("collections.outdated"); | |
164 SQL_DELETE_COLLECTION_ITEMS = sql.get("delete.collection.items"); | |
165 SQL_DELETE_COLLECTION = sql.get("delete.collection"); | |
166 SQL_DELETE_ARTIFACT = sql.get("artifacts.delete"); | |
167 } | |
168 | |
169 /** | |
170 * Sets the filter that prevents deletion of living artifacts. | |
171 * Living artifacts are artifacts which are currently active | |
172 * inside the artifact database. Deleting them in this state | |
173 * would create severe internal problems. | |
174 */ | |
175 public void setLockedIdsProvider(LockedIdsProvider lockedIdsProvider) { | |
176 this.lockedIdsProvider = lockedIdsProvider; | |
177 } | |
178 | |
179 /** | |
180 * External hook to tell the cleaner to wake up before its | |
181 * regular nap time is over. This is the case when the artifact | |
182 * database finds an artifact which is already outdated. | |
183 */ | |
184 public void wakeup() { | |
185 synchronized (sleepLock) { | |
186 sleepLock.notify(); | |
187 } | |
188 } | |
189 | |
190 /** | |
191 * Fetches the sleep time from the global configuration. | |
192 * @return the time to sleep between database cleanups in ms. | |
193 */ | |
194 protected static long getSleepTime() { | |
195 String sleepTimeString = Config.getStringXPath(SLEEP_XPATH); | |
196 | |
197 if (sleepTimeString == null) { | |
198 return SLEEP_DEFAULT; | |
199 } | |
200 try { | |
201 // sleep at least one second | |
202 return Math.max(Long.parseLong(sleepTimeString), 1000L); | |
203 } | |
204 catch (NumberFormatException nfe) { | |
205 logger.warn("Cleaner sleep time defaults to " + SLEEP_DEFAULT); | |
206 } | |
207 return SLEEP_DEFAULT; | |
208 } | |
209 | |
210 private static class IdIdentifier { | |
211 | |
212 int id; | |
213 String identifier; | |
214 | |
215 private IdIdentifier(int id, String identifier) { | |
216 this.id = id; | |
217 this.identifier = identifier; | |
218 } | |
219 } // class IdIdentifier | |
220 | |
221 private static final class IdData | |
222 extends IdIdentifier | |
223 { | |
224 byte [] data; | |
225 String factoryName; | |
226 | |
227 public IdData( | |
228 int id, | |
229 String factoryName, | |
230 byte [] data, | |
231 String identifier | |
232 ) { | |
233 super(id, identifier); | |
234 this.factoryName = factoryName; | |
235 this.data = data; | |
236 } | |
237 } // class IdData | |
238 | |
239 /** | |
240 * Cleaning is done in two phases. First we fetch a list of ids | |
241 * of artifacts. If there are artifacts the cleaning is done. | |
242 * Second we load the artifacts one by one one and call there | |
243 * endOfLife() method. In this loop we remove them from database, too. | |
244 * Each deletion is commited to ensure that a sudden failure | |
245 * of the artifact database server does delete artifacts twice | |
246 * or does not delete them at all. After this the first step | |
247 * is repeated. | |
248 */ | |
249 protected void cleanup() { | |
250 logger.info("database cleanup"); | |
251 | |
252 Connection connection = null; | |
253 PreparedStatement fetchIds = null; | |
254 PreparedStatement stmnt = null; | |
255 ResultSet result = null; | |
256 | |
257 DataSource dataSource = dbConnection.getDataSource(); | |
258 | |
259 Set<Integer> lockedIds = lockedIdsProvider != null | |
260 ? lockedIdsProvider.getLockedIds() | |
261 : EMPTY_IDS; | |
262 | |
263 String questionMarks = lockedIds.isEmpty() | |
264 ? "-666" // XXX: A bit hackish. | |
265 : StringUtils.repeat('?', lockedIds.size(), ','); | |
266 | |
267 List<String> deletedCollections = new ArrayList<String>(); | |
268 List<String> deletedArtifacts = new ArrayList<String>(); | |
269 | |
270 try { | |
271 connection = dataSource.getConnection(); | |
272 connection.setAutoCommit(false); | |
273 | |
274 fetchIds = connection.prepareStatement( | |
275 SQL_OUTDATED.replace("$LOCKED_IDS$", questionMarks)); | |
276 | |
277 // some dbms like derby do not support LIMIT | |
278 // in SQL statements. | |
279 fetchIds.setMaxRows(MAX_ROWS); | |
280 | |
281 // Fetch ids of outdated collections | |
282 stmnt = connection.prepareStatement( | |
283 SQL_OUTDATED_COLLECTIONS.replace( | |
284 "$LOCKED_IDS$", questionMarks)); | |
285 | |
286 // fill in the locked ids | |
287 int idx = 1; | |
288 for (Integer id: lockedIds) { | |
289 fetchIds.setInt(idx, id); | |
290 stmnt .setInt(idx, id); | |
291 ++idx; | |
292 } | |
293 | |
294 ArrayList<IdIdentifier> cs = new ArrayList<IdIdentifier>(); | |
295 result = stmnt.executeQuery(); | |
296 while (result.next()) { | |
297 cs.add(new IdIdentifier( | |
298 result.getInt(1), | |
299 result.getString(2))); | |
300 } | |
301 | |
302 result.close(); result = null; | |
303 stmnt.close(); stmnt = null; | |
304 | |
305 // delete collection items | |
306 stmnt = connection.prepareStatement(SQL_DELETE_COLLECTION_ITEMS); | |
307 | |
308 for (IdIdentifier id: cs) { | |
309 logger.debug("Mark collection for deletion: " + id.id); | |
310 stmnt.setInt(1, id.id); | |
311 stmnt.execute(); | |
312 } | |
313 | |
314 stmnt.close(); stmnt = null; | |
315 | |
316 // delete collections | |
317 stmnt = connection.prepareStatement(SQL_DELETE_COLLECTION); | |
318 | |
319 for (IdIdentifier id: cs) { | |
320 stmnt.setInt(1, id.id); | |
321 stmnt.execute(); | |
322 deletedCollections.add(id.identifier); | |
323 } | |
324 | |
325 stmnt.close(); stmnt = null; | |
326 connection.commit(); | |
327 | |
328 cs = null; | |
329 | |
330 // remove artifacts | |
331 stmnt = connection.prepareStatement(SQL_DELETE_ARTIFACT); | |
332 | |
333 for (;;) { | |
334 List<IdData> ids = new ArrayList<IdData>(); | |
335 | |
336 result = fetchIds.executeQuery(); | |
337 | |
338 while (result.next()) { | |
339 ids.add(new IdData( | |
340 result.getInt(1), | |
341 result.getString(2), | |
342 result.getBytes(3), | |
343 result.getString(4))); | |
344 } | |
345 | |
346 result.close(); result = null; | |
347 | |
348 if (ids.isEmpty()) { | |
349 break; | |
350 } | |
351 | |
352 for (int i = ids.size()-1; i >= 0; --i) { | |
353 IdData idData = ids.get(i); | |
354 Artifact artifact = reviver.reviveArtifact( | |
355 idData.factoryName, idData.data); | |
356 idData.data = null; | |
357 | |
358 logger.debug("Prepare Artifact (id=" | |
359 + idData.id + ") for deletion."); | |
360 | |
361 stmnt.setInt(1, idData.id); | |
362 stmnt.execute(); | |
363 connection.commit(); | |
364 | |
365 try { | |
366 if (artifact != null) { | |
367 logger.debug("Call endOfLife for Artifact: " | |
368 + artifact.identifier()); | |
369 | |
370 artifact.endOfLife(context); | |
371 } | |
372 } | |
373 catch (Exception e) { | |
374 logger.error(e.getMessage(), e); | |
375 } | |
376 | |
377 deletedArtifacts.add(idData.identifier); | |
378 } // for all fetched data | |
379 } | |
380 } | |
381 catch (SQLException sqle) { | |
382 logger.error(sqle.getLocalizedMessage(), sqle); | |
383 } | |
384 finally { | |
385 if (result != null) { | |
386 try { result.close(); } | |
387 catch (SQLException sqle) {} | |
388 } | |
389 if (stmnt != null) { | |
390 try { stmnt.close(); } | |
391 catch (SQLException sqle) {} | |
392 } | |
393 if (fetchIds != null) { | |
394 try { fetchIds.close(); } | |
395 catch (SQLException sqle) {} | |
396 } | |
397 if (connection != null) { | |
398 try { connection.close(); } | |
399 catch (SQLException sqle) {} | |
400 } | |
401 } | |
402 | |
403 if (!deletedCollections.isEmpty()) { | |
404 reviver.killedCollections(deletedCollections); | |
405 } | |
406 | |
407 if (!deletedArtifacts.isEmpty()) { | |
408 reviver.killedArtifacts(deletedArtifacts); | |
409 } | |
410 | |
411 if (logger.isDebugEnabled()) { | |
412 logger.debug( | |
413 "collections removed: " + deletedCollections.size()); | |
414 logger.debug( | |
415 "artifacts removed: " + deletedArtifacts.size()); | |
416 } | |
417 } | |
418 | |
419 /** | |
420 * The main code of the cleaner. It sleeps for the configured | |
421 * nap time, cleans up the database, sleeps again and so on. | |
422 */ | |
423 @Override | |
424 public void run() { | |
425 logger.info("sleep time: " + sleepTime + "ms"); | |
426 for (;;) { | |
427 cleanup(); | |
428 long startTime = System.currentTimeMillis(); | |
429 | |
430 try { | |
431 synchronized (sleepLock) { | |
432 sleepLock.wait(sleepTime); | |
433 } | |
434 } | |
435 catch (InterruptedException ie) { | |
436 } | |
437 | |
438 long stopTime = System.currentTimeMillis(); | |
439 | |
440 if (logger.isDebugEnabled()) { | |
441 logger.debug("Cleaner slept " + (stopTime - startTime) + "ms"); | |
442 } | |
443 } // for (;;) | |
444 } | |
445 } | |
446 // vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 : |