/
BaseHelper.java
317 lines (268 loc) · 12 KB
/
BaseHelper.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
/*
* Copyright (c) 2010-2017 Evolveum
*
* Licensed 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 com.evolveum.midpoint.repo.sql.helpers;
import com.evolveum.midpoint.repo.sql.*;
import com.evolveum.midpoint.repo.sql.data.common.any.RAnyConverter;
import com.evolveum.midpoint.repo.sql.util.RUtil;
import com.evolveum.midpoint.schema.result.OperationResult;
import com.evolveum.midpoint.schema.util.ExceptionUtil;
import com.evolveum.midpoint.util.backoff.BackoffComputer;
import com.evolveum.midpoint.util.backoff.ExponentialBackoffComputer;
import com.evolveum.midpoint.util.exception.SystemException;
import com.evolveum.midpoint.util.logging.Trace;
import com.evolveum.midpoint.util.logging.TraceManager;
import org.apache.commons.lang.StringUtils;
import org.hibernate.*;
import org.hibernate.exception.LockAcquisitionException;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.orm.hibernate5.HibernateOptimisticLockingFailureException;
import org.springframework.orm.hibernate5.LocalSessionFactoryBean;
import org.springframework.stereotype.Component;
import java.sql.SQLException;
import static com.evolveum.midpoint.repo.sql.SqlBaseService.LOCKING_EXP_THRESHOLD;
import static com.evolveum.midpoint.repo.sql.SqlBaseService.LOCKING_MAX_RETRIES;
import static com.evolveum.midpoint.repo.sql.SqlBaseService.LOCKING_DELAY_INTERVAL_BASE;
/**
* Core functionality needed in all members of SQL service family.
* Taken out of SqlBaseService in order to be accessible from other helpers without having to autowire SqlRepositoryServiceImpl
* (as it causes problems with Spring AOP proxies.)
*
* @author lazyman
* @author mederly
*/
@Component
public class BaseHelper {
private static final Trace LOGGER = TraceManager.getTrace(BaseHelper.class);
private static final Trace CONTENTION_LOGGER = TraceManager.getTrace(SqlRepositoryServiceImpl.CONTENTION_LOG_NAME);
@Autowired
private SessionFactory sessionFactory;
@Autowired
private SqlRepositoryFactory repositoryFactory;
@Autowired
private LocalSessionFactoryBean sessionFactoryBean;
public SessionFactory getSessionFactory() {
return sessionFactory;
}
public void setSessionFactory(SessionFactory sessionFactory) {
RUtil.fixCompositeIDHandling(sessionFactory);
this.sessionFactory = sessionFactory;
}
public LocalSessionFactoryBean getSessionFactoryBean() {
return sessionFactoryBean;
}
public Session beginReadOnlyTransaction() {
return beginTransaction(getConfiguration().isUseReadOnlyTransactions());
}
public Session beginTransaction() {
return beginTransaction(false);
}
public Session beginTransaction(boolean readOnly) {
Session session = getSessionFactory().openSession();
session.beginTransaction();
if (getConfiguration().getTransactionIsolation() == TransactionIsolation.SNAPSHOT) {
LOGGER.trace("Setting transaction isolation level SNAPSHOT.");
session.doWork(connection -> connection.createStatement().execute("SET TRANSACTION ISOLATION LEVEL SNAPSHOT"));
}
if (readOnly) {
// we don't want to flush changes during readonly transactions (they should never occur,
// but if they occur transaction commit would still fail)
session.setFlushMode(FlushMode.MANUAL);
LOGGER.trace("Marking transaction as read only.");
session.doWork(connection -> connection.createStatement().execute("SET TRANSACTION READ ONLY"));
}
return session;
}
public SqlRepositoryConfiguration getConfiguration() {
return repositoryFactory.getSqlConfiguration();
}
public void rollbackTransaction(Session session) {
rollbackTransaction(session, null, null, false);
}
public void rollbackTransaction(Session session, Exception ex, OperationResult result, boolean fatal) {
String message = ex != null ? ex.getMessage() : "null";
rollbackTransaction(session, ex, message, result, fatal);
}
public void rollbackTransaction(Session session, Exception ex, String message, OperationResult result,
boolean fatal) {
if (StringUtils.isEmpty(message) && ex != null) {
message = ex.getMessage();
}
// non-fatal errors will NOT be put into OperationResult, not to confuse the user
if (result != null && fatal) {
result.recordFatalError(message, ex);
}
if (session == null || session.getTransaction() == null || !session.getTransaction().isActive()) {
return;
}
session.getTransaction().rollback();
}
public void cleanupSessionAndResult(Session session, OperationResult result) {
if (session != null && session.isOpen()) {
session.close();
}
if (result != null && result.isUnknown()) {
result.computeStatus();
}
}
public void handleGeneralException(Exception ex, Session session, OperationResult result) {
if (ex instanceof RuntimeException) {
handleGeneralRuntimeException((RuntimeException) ex, session, result);
} else {
handleGeneralCheckedException(ex, session, result);
}
throw new IllegalStateException("Shouldn't get here"); // just a marker to be obvious that this method never returns normally
}
public void handleGeneralRuntimeException(RuntimeException ex, Session session, OperationResult result) {
LOGGER.debug("General runtime exception occurred.", ex);
if (isExceptionRelatedToSerialization(ex)) {
rollbackTransaction(session, ex, result, false);
// this exception will be caught and processed in logOperationAttempt,
// so it's safe to pass any RuntimeException here
throw ex;
} else {
rollbackTransaction(session, ex, result, true);
if (ex instanceof SystemException) {
throw ex;
} else {
throw new SystemException(ex.getMessage(), ex);
}
}
}
public void handleGeneralCheckedException(Exception ex, Session session, OperationResult result) {
LOGGER.error("General checked exception occurred.", ex);
boolean fatal = !isExceptionRelatedToSerialization(ex);
rollbackTransaction(session, ex, result, fatal);
throw new SystemException(ex.getMessage(), ex);
}
public int logOperationAttempt(String oid, String operation, int attempt, @NotNull RuntimeException ex,
OperationResult result) {
boolean serializationException = isExceptionRelatedToSerialization(ex);
if (!serializationException) {
// to be sure that we won't miss anything related to deadlocks, here is an ugly hack that checks it (with some probability...)
boolean serializationTextFound = ex.getMessage() != null && (exceptionContainsText(ex, "deadlock") || exceptionContainsText(ex, "could not serialize access"));
if (serializationTextFound) {
LOGGER.error("Transaction serialization-related problem (e.g. deadlock) was probably not caught correctly!", ex);
}
throw ex;
}
BackoffComputer backoffComputer = new ExponentialBackoffComputer(LOCKING_MAX_RETRIES, LOCKING_DELAY_INTERVAL_BASE, LOCKING_EXP_THRESHOLD, null);
long waitTime;
try {
waitTime = backoffComputer.computeDelay(attempt);
} catch (BackoffComputer.NoMoreRetriesException e) {
CONTENTION_LOGGER.error("A serialization-related problem occurred, maximum attempts ({}) reached.", attempt, ex);
LOGGER.error("A serialization-related problem occurred, maximum attempts ({}) reached.", attempt, ex);
if (result != null) {
result.recordFatalError("A serialization-related problem occurred.", ex);
}
throw new SystemException(ex.getMessage() + " [attempts: " + attempt + "]", ex);
}
String message = "A serialization-related problem occurred when {} object with oid '{}', retrying after "
+ "{} ms (this is retry {} of {})\n{}: {}";
Object[] objects = { operation, oid, waitTime, attempt, LOCKING_MAX_RETRIES, ex.getClass().getSimpleName(), ex.getMessage() };
if (attempt >= SqlRepositoryServiceImpl.CONTENTION_LOG_DEBUG_THRESHOLD) {
CONTENTION_LOGGER.debug(message, objects);
} else {
CONTENTION_LOGGER.trace(message, objects);
}
LOGGER.debug(message, objects);
if (waitTime > 0) {
try {
Thread.sleep(waitTime);
} catch (InterruptedException ex1) {
// ignore this
}
}
return attempt + 1;
}
private boolean isExceptionRelatedToSerialization(Exception ex) {
boolean rv = isExceptionRelatedToSerializationInternal(ex);
LOGGER.trace("Considering if exception {} is related to serialization: returning {}", ex, rv, ex);
return rv;
}
private boolean isExceptionRelatedToSerializationInternal(Exception ex) {
if (ex instanceof PessimisticLockException
|| ex instanceof LockAcquisitionException
|| ex instanceof HibernateOptimisticLockingFailureException
|| ex instanceof StaleObjectStateException) { // todo the last one is questionable
return true;
}
if (ExceptionUtil.findCause(ex, SerializationRelatedException.class) != null) {
return true;
}
// it's not locking exception (optimistic, pesimistic lock or simple lock acquisition) understood by hibernate
// however, it still could be such exception... wrapped in e.g. TransactionException
// so we have a look inside - we try to find SQLException there
SQLException sqlException = findSqlException(ex);
if (sqlException == null) {
return false;
}
// these error codes / SQL states we consider related to locking:
// code 50200 [table timeout lock in H2, 50200 is LOCK_TIMEOUT_1 error code]
// code 40001 [DEADLOCK_1 in H2]
// state 40001 [serialization failure in PostgreSQL - http://www.postgresql.org/docs/9.1/static/transaction-iso.html - and probably also in other systems]
// state 40P01 [deadlock in PostgreSQL]
// code ORA-08177: can't serialize access for this transaction in Oracle
// code ORA-01466 ["unable to read data - table definition has changed"] in Oracle
// code ORA-01555: snapshot too old: rollback segment number with name "" too small
// code ORA-22924: snapshot too old
//
// sql states should be somewhat standardized; sql error codes are vendor-specific
// todo: so it is probably not very safe to test for codes without testing for specific database (h2, oracle)
// but the risk of problem is quite low here, so let it be...
// strange exception occurring in MySQL when doing multithreaded org closure maintenance
// alternatively we might check for error code = 1030, sql state = HY000
// but that would cover all cases of "Got error XYZ from storage engine"
if (getConfiguration().isUsingMySqlCompatible()
&& sqlException.getMessage() != null
&& sqlException.getMessage().contains("Got error -1 from storage engine")) {
return true;
}
// this is some recent H2 weirdness (MID-3969)
if (getConfiguration().isUsingH2() && sqlException.getMessage() != null
&& sqlException.getMessage().contains("Referential integrity constraint violation: \"FK_AUDIT_ITEM: PUBLIC.M_AUDIT_ITEM FOREIGN KEY(RECORD_ID) REFERENCES PUBLIC.M_AUDIT_EVENT(ID)")) {
return true;
}
return sqlException.getErrorCode() == 50200
|| sqlException.getErrorCode() == 40001
|| "40001".equals(sqlException.getSQLState())
|| "40P01".equals(sqlException.getSQLState())
|| sqlException.getErrorCode() == 8177
|| sqlException.getErrorCode() == 1466
|| sqlException.getErrorCode() == 1555
|| sqlException.getErrorCode() == 22924
|| sqlException.getErrorCode() == 3960; // Snapshot isolation transaction aborted due to update conflict.
}
public SQLException findSqlException(Throwable ex) {
while (ex != null) {
if (ex instanceof SQLException) {
return (SQLException) ex;
}
ex = ex.getCause();
}
return null;
}
private boolean exceptionContainsText(Throwable ex, String text) {
while (ex != null) {
if (ex.getMessage() != null && ex.getMessage().contains(text)) {
return true;
}
ex = ex.getCause();
}
return false;
}
}