Skip to content

Commit aba72d0

Browse files
committed
move logic of db triggers for setting user.inTransaction to java code
reason: with this commit, we reduce the possibility of deadlocks when processing highly concurrent start- and stopTransaction messages while testing with abusive-charge-point (https://github.com/chargegrid/abusive-charge-point). the problem was not concurrent access of the transaction table (as previously incorrectly guessed), but the ocpp tag triggers attached to transaction table to set ocppTag.inTransaction=true|false after each insert/update. the deadlocks happen, because 1) abusive-charge-point sends multiple transaction messages at the same time (millisecond precision), 2) all these transaction messages use the same id tag, => triggers in db try to set ocppTag.inTransaction field of the same ROW at the same TIME. the solution is to split the previously long db transaction (transaction + ocpp tag table operations) into two smaller ones, so the locks are released sooner.
1 parent 033c5fe commit aba72d0

File tree

2 files changed

+70
-36
lines changed

2 files changed

+70
-36
lines changed

src/main/java/de/rwth/idsg/steve/repository/impl/OcppServerRepositoryImpl.java

Lines changed: 67 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package de.rwth.idsg.steve.repository.impl;
22

3+
import de.rwth.idsg.steve.SteveException;
34
import de.rwth.idsg.steve.repository.OcppServerRepository;
45
import de.rwth.idsg.steve.repository.ReservationRepository;
56
import de.rwth.idsg.steve.repository.dto.InsertConnectorStatusParams;
@@ -44,12 +45,6 @@ public class OcppServerRepositoryImpl implements OcppServerRepository {
4445
@Autowired private DSLContext ctx;
4546
@Autowired private ReservationRepository reservationRepository;
4647

47-
// true story: while testing with abusive-charge-point, from time to time, we get MySql exceptions with
48-
// "Deadlock found when trying to get lock; try restarting transaction" when processing startTransaction and/or
49-
// stopTransaction messages.
50-
// TODO: this is an initial dirty solution/workaround. look into the cause of the problem.
51-
private final Object TRANSACTION_LOCK = new Object();
52-
5348
@Override
5449
public void updateChargebox(UpdateChargeboxParams p) {
5550
ctx.update(CHARGE_BOX)
@@ -161,10 +156,6 @@ public void insertMeterValues(String chargeBoxIdentity, List<MeterValue> list, i
161156

162157
@Override
163158
public Integer insertTransaction(InsertTransactionParams p) {
164-
insertIgnoreConnector(ctx, p.getChargeBoxId(), p.getConnectorId());
165-
166-
// it is important to insert idTag before transaction, since the transaction table references it
167-
boolean unknownTagInserted = insertIgnoreIdTag(ctx, p);
168159

169160
SelectConditionStep<Record1<Integer>> connectorPkQuery =
170161
DSL.select(CONNECTOR.CONNECTOR_PK)
@@ -173,20 +164,30 @@ public Integer insertTransaction(InsertTransactionParams p) {
173164
.and(CONNECTOR.CONNECTOR_ID.equal(p.getConnectorId()));
174165

175166
// -------------------------------------------------------------------------
176-
// Step 1: Insert transaction
167+
// Step 1: Insert connector and idTag, if they are new to us
168+
// -------------------------------------------------------------------------
169+
170+
insertIgnoreConnector(ctx, p.getChargeBoxId(), p.getConnectorId());
171+
172+
// it is important to insert idTag before transaction, since the transaction table references it
173+
boolean unknownTagInserted = insertIgnoreIdTag(ctx, p);
174+
175+
// -------------------------------------------------------------------------
176+
// Step 2: Insert transaction
177177
// -------------------------------------------------------------------------
178178

179-
int transactionId;
180-
181-
synchronized (TRANSACTION_LOCK) {
182-
transactionId = ctx.insertInto(TRANSACTION)
183-
.set(TRANSACTION.CONNECTOR_PK, connectorPkQuery)
184-
.set(TRANSACTION.ID_TAG, p.getIdTag())
185-
.set(TRANSACTION.START_TIMESTAMP, p.getStartTimestamp())
186-
.set(TRANSACTION.START_VALUE, p.getStartMeterValue())
187-
.returning(TRANSACTION.TRANSACTION_PK)
188-
.fetchOne()
189-
.getTransactionPk();
179+
Integer transactionId = ctx.insertInto(TRANSACTION)
180+
.set(TRANSACTION.CONNECTOR_PK, connectorPkQuery)
181+
.set(TRANSACTION.ID_TAG, p.getIdTag())
182+
.set(TRANSACTION.START_TIMESTAMP, p.getStartTimestamp())
183+
.set(TRANSACTION.START_VALUE, p.getStartMeterValue())
184+
.returning(TRANSACTION.TRANSACTION_PK)
185+
.fetchOne()
186+
.getTransactionPk();
187+
188+
// Actually unnecessary, because JOOQ will throw an exception, if something goes wrong
189+
if (transactionId == null) {
190+
throw new SteveException("Failed to INSERT transaction into database");
190191
}
191192

192193
if (unknownTagInserted) {
@@ -195,15 +196,28 @@ public Integer insertTransaction(InsertTransactionParams p) {
195196
}
196197

197198
// -------------------------------------------------------------------------
198-
// Step 2 for OCPP 1.5: A startTransaction may be related to a reservation
199+
// Step 3: Update OCPP tag (in_transaction=true)
200+
// -------------------------------------------------------------------------
201+
202+
int count = ctx.update(OCPP_TAG)
203+
.set(OCPP_TAG.IN_TRANSACTION, true)
204+
.where(OCPP_TAG.ID_TAG.eq(p.getIdTag()))
205+
.execute();
206+
207+
if (count == 0) {
208+
log.warn("Failed to set in_transaction=true of OCPP tag of STARTED transaction {}", transactionId);
209+
}
210+
211+
// -------------------------------------------------------------------------
212+
// Step 4 for OCPP >= 1.5: A startTransaction may be related to a reservation
199213
// -------------------------------------------------------------------------
200214

201215
if (p.isSetReservationId()) {
202216
reservationRepository.used(connectorPkQuery, p.getIdTag(), p.getReservationId(), transactionId);
203217
}
204218

205219
// -------------------------------------------------------------------------
206-
// Step 3: Set connector status to "Occupied"
220+
// Step 5: Set connector status to "Occupied"
207221
// -------------------------------------------------------------------------
208222

209223
insertConnectorStatus(ctx, connectorPkQuery, p.getStartTimestamp(), p.getStatusUpdate());
@@ -216,23 +230,40 @@ public void updateTransaction(UpdateTransactionParams p) {
216230

217231
// -------------------------------------------------------------------------
218232
// Step 1: Update transaction table
219-
//
220-
// After update, a DB trigger sets the user.inTransaction field to 0
221233
// -------------------------------------------------------------------------
222234

223-
synchronized (TRANSACTION_LOCK) {
224-
ctx.update(TRANSACTION)
225-
.set(TRANSACTION.STOP_TIMESTAMP, p.getStopTimestamp())
226-
.set(TRANSACTION.STOP_VALUE, p.getStopMeterValue())
227-
.set(TRANSACTION.STOP_REASON, p.getStopReason())
228-
.where(TRANSACTION.TRANSACTION_PK.equal(p.getTransactionId()))
229-
.and(TRANSACTION.STOP_TIMESTAMP.isNull())
230-
.and(TRANSACTION.STOP_VALUE.isNull())
231-
.execute();
235+
int transactionUpdateCount = ctx.update(TRANSACTION)
236+
.set(TRANSACTION.STOP_TIMESTAMP, p.getStopTimestamp())
237+
.set(TRANSACTION.STOP_VALUE, p.getStopMeterValue())
238+
.set(TRANSACTION.STOP_REASON, p.getStopReason())
239+
.where(TRANSACTION.TRANSACTION_PK.equal(p.getTransactionId()))
240+
.execute();
241+
242+
// Actually unnecessary, because JOOQ will throw an exception, if something goes wrong
243+
if (transactionUpdateCount == 0) {
244+
throw new SteveException("Failed to UPDATE transaction in database");
245+
}
246+
247+
// -------------------------------------------------------------------------
248+
// Step 2: Update OCPP tag (in_transaction=false)
249+
// -------------------------------------------------------------------------
250+
251+
SelectConditionStep<Record1<String>> idTagSelect =
252+
DSL.select(TRANSACTION.ID_TAG)
253+
.from(TRANSACTION)
254+
.where(TRANSACTION.TRANSACTION_PK.equal(p.getTransactionId()));
255+
256+
int ocppTagUpdateCount = ctx.update(OCPP_TAG)
257+
.set(OCPP_TAG.IN_TRANSACTION, false)
258+
.where(OCPP_TAG.ID_TAG.eq(idTagSelect))
259+
.execute();
260+
261+
if (ocppTagUpdateCount == 0) {
262+
log.warn("Failed to set in_transaction=false of OCPP tag of STOPPED transaction {}", p.getTransactionId());
232263
}
233264

234265
// -------------------------------------------------------------------------
235-
// Step 2: Set connector status back to "Available" again
266+
// Step 3: Set connector status back to "Available" again
236267
// -------------------------------------------------------------------------
237268

238269
SelectConditionStep<Record1<Integer>> connectorPkQuery =
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
DROP TRIGGER IF EXISTS `transaction_AINS`;
2+
3+
DROP TRIGGER IF EXISTS `transaction_AUPD`;

0 commit comments

Comments
 (0)