Skip to content

Commit eaa59ff

Browse files
TRUNK-6409: Initializer module should not re-run initialization on each node
1 parent 0d07741 commit eaa59ff

9 files changed

Lines changed: 546 additions & 4 deletions

File tree

api/src/main/java/org/openmrs/module/initializer/InitializerActivator.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,14 +82,39 @@ public void started() {
8282
log.info("OpenMRS config loading process disabled at initializer startup");
8383
} else {
8484
boolean throwError = PROPS_STARTUP_LOAD_FAIL_ON_ERROR.equalsIgnoreCase(startupLoadingMode);
85-
log.info("OpenMRS config loading process started...");
85+
86+
// Try to acquire DB lock
87+
String nodeId = getInitializerService().getOrCreateNodeId();
88+
if (!getInitializerService().tryAcquireLock(nodeId)) {
89+
log.info("Another node is running initializer... skipping");
90+
return;
91+
}
92+
8693
try {
94+
// Check if config changed
95+
if (!getInitializerService().isConfigChanged()) {
96+
log.info("No config changes... skipping initializer");
97+
return;
98+
}
99+
100+
// Run Initializer
101+
log.info("OpenMRS config loading process started...");
102+
log.info("Initializer lock acquired by {}", nodeId);
87103
getInitializerService().loadUnsafe(true, throwError);
104+
105+
// Save new checksums
106+
getInitializerService().updateChecksums();
88107
log.info("OpenMRS config loading process completed.");
89108
}
90109
catch (Exception e) {
110+
log.error("Initializer failed", e);
91111
throw new ModuleException("An error occurred loading initializer configuration", e);
92112
}
113+
finally {
114+
// Always release lock
115+
getInitializerService().releaseLock(nodeId);
116+
log.info("Initializer lock released");
117+
}
93118
}
94119
}
95120

api/src/main/java/org/openmrs/module/initializer/api/HibernateInitializerDAO.java

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package org.openmrs.module.initializer.api;
22

33
import java.util.Collections;
4+
import java.util.Date;
45
import java.util.List;
5-
import java.util.stream.Collectors;
66

77
import org.apache.commons.lang3.StringUtils;
88
import org.hibernate.Criteria;
@@ -13,10 +13,9 @@
1313
import org.openmrs.ConceptName;
1414
import org.openmrs.api.ConceptNameType;
1515
import org.openmrs.api.context.Context;
16+
import org.openmrs.module.initializer.api.entities.InitializerChecksum;
1617
import org.slf4j.Logger;
1718
import org.slf4j.LoggerFactory;
18-
import org.springframework.beans.factory.annotation.Autowired;
19-
import org.springframework.stereotype.Component;
2019

2120
/**
2221
* The Hibernate class for database related functions <br>
@@ -29,6 +28,8 @@ public class HibernateInitializerDAO implements InitializerDAO {
2928

3029
private static final Logger log = LoggerFactory.getLogger(HibernateInitializerDAO.class);
3130

31+
private static final long LOCK_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
32+
3233
private SessionFactory sessionFactory;
3334

3435
/**
@@ -67,4 +68,64 @@ public List<Concept> getUnretiredConceptsByFullySpecifiedName(String name) {
6768
List<Concept> list = criteria.list();
6869
return list;
6970
}
71+
72+
/**
73+
* @see org.openmrs.module.initializer.api.InitializerService#tryAcquireLock(String)
74+
*/
75+
@Override
76+
public Boolean tryAcquireLock(String nodeId) {
77+
long now = System.currentTimeMillis();
78+
String hql = "UPDATE InitializerLock " + "SET locked = true, lockedAt = current_timestamp, lockedBy = :node "
79+
+ "WHERE id = 1 AND (locked = false OR lockedAt < :expiryTime)";
80+
81+
Date expiryTime = new Date(now - LOCK_TIMEOUT_MS);
82+
int updated = sessionFactory.getCurrentSession().createQuery(hql).setParameter("node", nodeId)
83+
.setParameter("expiryTime", expiryTime).executeUpdate();
84+
85+
return updated == 1;
86+
}
87+
88+
/**
89+
* @see org.openmrs.module.initializer.api.InitializerService#releaseLock(String)
90+
*/
91+
@Override
92+
public void releaseLock(String nodeId) {
93+
String hql = "UPDATE InitializerLock " + "SET locked = false, lockedAt = null, lockedBy = null "
94+
+ "WHERE id = 1 AND lockedBy = :node";
95+
sessionFactory.getCurrentSession().createQuery(hql).setParameter("node", nodeId).executeUpdate();
96+
}
97+
98+
/**
99+
* @see org.openmrs.module.initializer.api.InitializerService#forceReleaseLock()
100+
*/
101+
public void forceReleaseLock() {
102+
String hql = "UPDATE InitializerLock " + "SET locked = false, lockedAt = null, lockedBy = null " + "WHERE id = 1";
103+
sessionFactory.getCurrentSession().createQuery(hql).executeUpdate();
104+
}
105+
106+
/**
107+
* @see org.openmrs.module.initializer.api.InitializerService#isLocked()
108+
*/
109+
@Override
110+
public Boolean isLocked() {
111+
Boolean locked = (Boolean) sessionFactory.getCurrentSession()
112+
.createQuery("SELECT locked FROM InitializerLock WHERE id = 1").uniqueResult();
113+
return Boolean.TRUE.equals(locked);
114+
}
115+
116+
@Override
117+
@SuppressWarnings("unchecked")
118+
public List<InitializerChecksum> getAll() {
119+
return sessionFactory.getCurrentSession().createQuery("FROM InitializerChecksum").list();
120+
}
121+
122+
@Override
123+
public void deleteAll() {
124+
sessionFactory.getCurrentSession().createQuery("DELETE FROM InitializerChecksum").executeUpdate();
125+
}
126+
127+
@Override
128+
public void saveOrUpdate(InitializerChecksum checksum) {
129+
sessionFactory.getCurrentSession().saveOrUpdate(checksum);
130+
}
70131
}

api/src/main/java/org/openmrs/module/initializer/api/InitializerDAO.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.util.List;
44

55
import org.openmrs.Concept;
6+
import org.openmrs.module.initializer.api.entities.InitializerChecksum;
67

78
/**
89
* For database related functions
@@ -15,4 +16,31 @@ public interface InitializerDAO {
1516
* @see org.openmrs.module.initializer.api.InitializerService#getUnretiredConceptsByFullySpecifiedName(String)
1617
*/
1718
public List<Concept> getUnretiredConceptsByFullySpecifiedName(String name);
19+
20+
/**
21+
* @see org.openmrs.module.initializer.api.InitializerService#tryAcquireLock(String)
22+
*/
23+
Boolean tryAcquireLock(String nodeId);
24+
25+
/**
26+
* @see org.openmrs.module.initializer.api.InitializerService#releaseLock(String)
27+
*/
28+
void releaseLock(String nodeId);
29+
30+
/**
31+
* @see org.openmrs.module.initializer.api.InitializerService#forceReleaseLock()
32+
*/
33+
void forceReleaseLock();
34+
35+
/**
36+
* @see org.openmrs.module.initializer.api.InitializerService#isLocked()
37+
*/
38+
Boolean isLocked();
39+
40+
List<InitializerChecksum> getAll();
41+
42+
void deleteAll();
43+
44+
void saveOrUpdate(InitializerChecksum checksum);
45+
1846
}

api/src/main/java/org/openmrs/module/initializer/api/InitializerService.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,58 @@ public interface InitializerService extends OpenmrsService {
132132
* @return the found Concepts
133133
*/
134134
List<Concept> getUnretiredConceptsByFullySpecifiedName(String name);
135+
136+
/**
137+
* Returns the persistent unique identifier for this node (never null) If no node identifier exists
138+
* yet, a new one is generated and persisted.
139+
*
140+
* @return the unique identifier of current node.
141+
*/
142+
String getOrCreateNodeId();
143+
144+
/**
145+
* Determines whether the initializer configuration has changed since the last successful execution.
146+
*
147+
* @return true or false if configuration changes are detected and the initializer should run or
148+
* otherwise.
149+
*/
150+
Boolean isConfigChanged();
151+
152+
/**
153+
* Updates and persists the current configuration checksums after a successful initializer
154+
* execution.
155+
*/
156+
void updateChecksums();
157+
158+
/**
159+
* Attempts to acquire the initializer lock for the given node. This prevents multiple nodes from
160+
* executing the initializer concurrently in a clustered environment. This is a timeout-based lock
161+
* which releases in case of a node crash so that the lock is not stuck forever or if a node holds
162+
* it for too long.
163+
*
164+
* @param nodeId the identifier of the node attempting to acquire the lock.
165+
* @return true if the lock was successfully acquired or false if another node currently holds the
166+
* lock.
167+
*/
168+
Boolean tryAcquireLock(String nodeId);
169+
170+
/**
171+
* Releases the initializer lock held by the given node.
172+
*
173+
* @param nodeId the identifier of the node releasing the lock.
174+
*/
175+
void releaseLock(String nodeId);
176+
177+
/**
178+
* Forces the release of the initializer lock regardless, useful for manual intervention, recovery
179+
* from node crashes, or administrative operations.
180+
*/
181+
void forceReleaseLock();
182+
183+
/**
184+
* Indicates whether the initializer is currently locked.
185+
*
186+
* @return true or false if the initializer lock is currently held or otherwise.
187+
*/
188+
Boolean isLocked();
135189
}

api/src/main/java/org/openmrs/module/initializer/api/InitializerServiceImpl.java

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,30 @@
1414

1515
import java.io.File;
1616
import java.io.InputStream;
17+
import java.nio.file.Files;
1718
import java.nio.file.Path;
1819
import java.nio.file.Paths;
1920
import java.util.ArrayList;
2021
import java.util.Collections;
22+
import java.util.Date;
2123
import java.util.HashMap;
2224
import java.util.List;
2325
import java.util.Map;
2426
import java.util.Set;
27+
import java.util.UUID;
2528
import java.util.stream.Collectors;
2629

30+
import org.apache.commons.codec.digest.DigestUtils;
2731
import org.apache.commons.lang3.BooleanUtils;
2832
import org.apache.commons.lang3.StringUtils;
2933
import org.codehaus.jackson.map.ObjectMapper;
3034
import org.openmrs.Concept;
3135
import org.openmrs.PersonAttributeType;
36+
import org.openmrs.api.AdministrationService;
3237
import org.openmrs.api.context.Context;
3338
import org.openmrs.api.impl.BaseOpenmrsService;
3439
import org.openmrs.module.initializer.InitializerConfig;
40+
import org.openmrs.module.initializer.api.entities.InitializerChecksum;
3541
import org.openmrs.module.initializer.api.loaders.Loader;
3642
import org.openmrs.module.initializer.api.utils.Utils;
3743
import org.openmrs.util.OpenmrsUtil;
@@ -49,6 +55,8 @@ public class InitializerServiceImpl extends BaseOpenmrsService implements Initia
4955

5056
private InitializerDAO initializerDAO;
5157

58+
private static final String GP_NODE_ID = "initializer.node.id";
59+
5260
public void setConfig(InitializerConfig cfg) {
5361
this.cfg = cfg;
5462
}
@@ -223,4 +231,113 @@ public InitializerConfig getInitializerConfig() {
223231
public List<Concept> getUnretiredConceptsByFullySpecifiedName(String name) {
224232
return initializerDAO.getUnretiredConceptsByFullySpecifiedName(name);
225233
}
234+
235+
/**
236+
* @see org.openmrs.module.initializer.api.InitializerService#getOrCreateNodeId()
237+
*/
238+
@Override
239+
public String getOrCreateNodeId() {
240+
AdministrationService admin = Context.getAdministrationService();
241+
String nodeId = admin.getGlobalProperty(GP_NODE_ID);
242+
243+
if (StringUtils.isBlank(nodeId)) {
244+
nodeId = UUID.randomUUID().toString();
245+
admin.setGlobalProperty(GP_NODE_ID, nodeId);
246+
}
247+
return nodeId;
248+
}
249+
250+
/**
251+
* @see org.openmrs.module.initializer.api.InitializerService#isConfigChanged()
252+
*/
253+
@Override
254+
public Boolean isConfigChanged() {
255+
Map<String, String> db = loadChecksumsFromDb();
256+
Map<String, String> fs = computeFileChecksums();
257+
258+
return !db.equals(fs);
259+
}
260+
261+
/**
262+
* @see org.openmrs.module.initializer.api.InitializerService#updateChecksums()
263+
*/
264+
@Override
265+
public void updateChecksums() {
266+
Map<String, String> checksums = computeFileChecksums();
267+
initializerDAO.deleteAll();
268+
269+
for (Map.Entry<String, String> e : checksums.entrySet()) {
270+
InitializerChecksum cs = new InitializerChecksum();
271+
cs.setFilePath(e.getKey());
272+
cs.setChecksum(e.getValue());
273+
cs.setUpdatedAt(new Date());
274+
initializerDAO.saveOrUpdate(cs);
275+
}
276+
}
277+
278+
/**
279+
* @see org.openmrs.module.initializer.api.InitializerService#tryAcquireLock(String)
280+
*/
281+
@Override
282+
public Boolean tryAcquireLock(String nodeId) {
283+
return initializerDAO.tryAcquireLock(nodeId);
284+
}
285+
286+
/**
287+
* @see org.openmrs.module.initializer.api.InitializerService#releaseLock(String)
288+
*/
289+
@Override
290+
public void releaseLock(String nodeId) {
291+
initializerDAO.releaseLock(nodeId);
292+
}
293+
294+
/**
295+
* @see org.openmrs.module.initializer.api.InitializerService#forceReleaseLock()
296+
*/
297+
@Override
298+
public void forceReleaseLock() {
299+
initializerDAO.forceReleaseLock();
300+
}
301+
302+
/**
303+
* @see org.openmrs.module.initializer.api.InitializerService#isLocked()
304+
*/
305+
@Override
306+
public Boolean isLocked() {
307+
return initializerDAO.isLocked();
308+
}
309+
310+
private Map<String, String> loadChecksumsFromDb() {
311+
List<InitializerChecksum> list = initializerDAO.getAll();
312+
Map<String, String> map = new HashMap<>();
313+
314+
for (InitializerChecksum cs : list) {
315+
map.put(cs.getFilePath(), cs.getChecksum());
316+
}
317+
return map;
318+
}
319+
320+
private Map<String, String> computeFileChecksums() {
321+
ConfigDirUtil util = new ConfigDirUtil(getConfigDirPath(), getChecksumsDirPath(), "");
322+
Path base = Paths.get(getConfigDirPath());
323+
List<File> files = util.getFiles("", Collections.emptyList());
324+
Map<String, String> map = new HashMap<>();
325+
326+
for (File f : files) {
327+
if (!f.isFile()) {
328+
continue;
329+
}
330+
331+
try {
332+
Path rel = base.relativize(f.toPath());
333+
byte[] bytes = Files.readAllBytes(f.toPath());
334+
map.put(rel.toString(), DigestUtils.sha256Hex(bytes));
335+
}
336+
catch (Exception e) {
337+
map.put(f.getName(), "ERROR");
338+
}
339+
}
340+
341+
return map;
342+
}
226343
}

0 commit comments

Comments
 (0)