Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
23fdea6
test(concurrency): add reproductions for 17 follow-up concurrency haz…
hongwei1 Jun 23, 2026
a84a7f2
fix(concurrency): propagate bulk-payment batch-reference claim failur…
hongwei1 Jun 23, 2026
d98c9d7
fix(concurrency): make consent status transitions atomic (H1, H2, H3,…
hongwei1 Jun 23, 2026
bd4861f
fix(concurrency): harden mutable singletons and lazy-init vars (H5, H…
hongwei1 Jun 23, 2026
1f55469
fix(concurrency): make Redis rate-limit and idempotency ops atomic (H…
hongwei1 Jun 23, 2026
b867544
fix(concurrency): guard business status transitions and lock TR statu…
hongwei1 Jun 23, 2026
609a17b
docs(concurrency): document batch-2 hazards (C1, H1-H7, M1-M9) and fixes
hongwei1 Jun 23, 2026
32eb4b6
test(concurrency): fix M5 race test to use conditional UPDATE path
hongwei1 Jun 25, 2026
4ee49bd
chore: remove sequential test runner (superseded by run_tests_paralle…
hongwei1 Jun 26, 2026
5a9c69d
refactor(context): extract processUacAnswer to reduce cognitive compl…
hongwei1 Jun 26, 2026
e33eb5b
fix(concurrency): return 404 not 400 for missing transaction request …
hongwei1 Jun 26, 2026
0df31c4
fix(consent): fail visibly when skip-SCA consent vanishes after creation
hongwei1 Jun 26, 2026
13f2479
fix(errors): replace bare-string Failure messages with OBP-XXX codes
hongwei1 Jul 1, 2026
678f61e
fix(consent): apply skip-SCA guarded auto-accept to v5.0.0 and v5.1.0…
hongwei1 Jul 2, 2026
906eb23
fix(account-access): win the status CAS before granting view access o…
hongwei1 Jul 2, 2026
6a44fa6
fix(rate-limit): self-heal expiry-less counter keys and return TTL at…
hongwei1 Jul 2, 2026
1969e92
fix(bulk-payment): release the batch_reference when parent TR creatio…
hongwei1 Jul 2, 2026
d2e1f21
fix(doobie): bump updatedat in conditional status UPDATEs to match sa…
hongwei1 Jul 2, 2026
0bb830f
fix(concurrency): warn when the transaction-request lock degrades to …
hongwei1 Jul 2, 2026
65a9260
refactor(concurrency): dedup challenge CAS block, align row-count che…
hongwei1 Jul 2, 2026
8d94a12
fix(test-runner): make the parallel runner's verdict trustworthy
hongwei1 Jul 2, 2026
4008bf8
fix(berlin-group): wrap PIIS example bodies explicitly for json4s
hongwei1 Jul 2, 2026
a2aada8
merge: PIIS example-body json4s fix from worktree investigation
hongwei1 Jul 2, 2026
30afe2a
fix(berlin-group): wrap all v1.3 ResourceDoc example bodies in Jvalue…
hongwei1 Jul 2, 2026
6c6935a
merge: extend PIIS example-body json4s fix to AIS/PIS/SigningBaskets
hongwei1 Jul 2, 2026
e812d3a
fix(test-runner): close three local-vs-CI coverage gaps
hongwei1 Jul 2, 2026
dded2d2
refactor(doobie): rename generic rowId parameters to their entity ids
hongwei1 Jul 2, 2026
91602f8
refactor(doobie): rename consentRowId to consentPrimaryKey
hongwei1 Jul 2, 2026
1435f9f
refactor(dynamic-entity): rename getReadableRowIds to getReadableDyna…
hongwei1 Jul 2, 2026
19bc3cd
chore: remove switch_to_java11.sh (unused, unreferenced)
hongwei1 Jul 2, 2026
f3c59c9
chore: restore run_all_tests.sh as a thin forward to run_tests_parall…
hongwei1 Jul 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package code.accountaccessrequest

import java.util.Date
import code.api.util.ErrorMessages
import code.util.{MappedUUID, UUIDString}
import com.openbankproject.commons.model.enums.AccountAccessRequestStatus
import net.liftweb.common.{Box, Full}
import net.liftweb.common.{Box, Failure, Full}
import net.liftweb.mapper._
import net.liftweb.util.Helpers.tryo

Expand Down Expand Up @@ -90,13 +91,12 @@ object MappedAccountAccessRequestProvider extends AccountAccessRequestProvider {
checkerComment: String
): Box[AccountAccessRequestTrait] = {
AccountAccessRequest.find(By(AccountAccessRequest.AccountAccessRequestId, accountAccessRequestId)).flatMap { request =>
tryo {
request
.Status(status)
.CheckerUserId(checkerUserId)
.CheckerComment(checkerComment)
.saveMe()
}
// Atomic guarded transition: an access request is actioned once, from INITIATED. The loser of a
// concurrent approve/decline gets 0 rows -> Failure, instead of silently overwriting the decision.
val rows = code.bankconnectors.DoobieBusinessStatusQueries.conditionalAccountAccessRequestStatus(
request.id.get, AccountAccessRequestStatus.INITIATED.toString, status, checkerUserId, checkerComment)
if (rows == 1) AccountAccessRequest.find(By(AccountAccessRequest.AccountAccessRequestId, accountAccessRequestId))
else Failure(ErrorMessages.AccountAccessRequestStatusNotInitiated)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,16 @@ object MappedAccountApplicationProvider extends AccountApplicationProvider {
match {
case Full(accountApplication) if(accountApplication.status == "ACCEPTED") =>
Failure(s"${ErrorMessages.AccountApplicationAlreadyAccepted} Current Account-Application-Id($accountApplicationId)")
case Full(accountApplication) => tryo{accountApplication.mStatus(status).saveMe()}
case Full(accountApplication) =>
// Optimistic CAS: transition only if the status hasn't changed since we loaded it. Two
// concurrent updates that both read the same status can no longer both write — the loser
// (0 rows) gets a Failure instead of silently overwriting the winner's decision.
val rows = code.bankconnectors.DoobieBusinessStatusQueries.conditionalAccountApplicationStatus(
accountApplication.id.get, accountApplication.status, status)
if (rows == 1) MappedAccountApplication.find(By(MappedAccountApplication.mAccountApplicationId, accountApplicationId))
// Use the generic update-failure code here: the concurrent winner may have written any
// status (ACCEPTED or REJECTED), so the "already accepted" message would be misleading.
else Failure(s"${ErrorMessages.UpdateAccountApplicationStatusError} Status changed concurrently. Current Account-Application-Id($accountApplicationId)")
case Empty => Failure(s"${ErrorMessages.AccountApplicationNotFound} Current Account-Application-Id($accountApplicationId)")
case _ => Failure(ErrorMessages.UnknownError)
}
Expand Down
23 changes: 16 additions & 7 deletions obp-api/src/main/scala/code/actorsystem/ObpActorSystem.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import com.typesafe.config.ConfigFactory
object ObpActorSystem extends MdcLoggable {

val props_hostname = Helper.getHostname
var obpActorSystem: ActorSystem = _
var northSideAkkaConnectorActorSystem: ActorSystem = _
// @volatile so the single assignment of each actor system is visible to all reader threads
// (the JVM memory model does not guarantee visibility of a non-volatile write across threads).
@volatile var obpActorSystem: ActorSystem = _
@volatile var northSideAkkaConnectorActorSystem: ActorSystem = _

def startLocalActorSystem() = localActorSystem

Expand All @@ -22,12 +24,19 @@ object ObpActorSystem extends MdcLoggable {
obpActorSystem = ActorSystem.create(s"ObpActorSystem_${props_hostname}", ConfigFactory.load(ConfigFactory.parseString(localConf)))
obpActorSystem
}


// synchronized double-checked init so concurrent callers start the connector system exactly once.
def startNorthSideAkkaConnectorActorSystem(): ActorSystem = {
logger.info("Starting North Side Akka Connector actor system")
val localConf = AkkaConnectorActorConfig.localConf
logger.info(localConf)
northSideAkkaConnectorActorSystem = ActorSystem.create(s"SouthSideAkkaConnector_${props_hostname}", ConfigFactory.load(ConfigFactory.parseString(localConf)))
if (northSideAkkaConnectorActorSystem == null) {
synchronized {
if (northSideAkkaConnectorActorSystem == null) {
logger.info("Starting North Side Akka Connector actor system")
val localConf = AkkaConnectorActorConfig.localConf
logger.info(localConf)
northSideAkkaConnectorActorSystem = ActorSystem.create(s"SouthSideAkkaConnector_${props_hostname}", ConfigFactory.load(ConfigFactory.parseString(localConf)))
}
}
}
northSideAkkaConnectorActorSystem
}
}
16 changes: 11 additions & 5 deletions obp-api/src/main/scala/code/actorsystem/ObpLookupSystem.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,20 @@ object ObpLookupSystem extends ObpLookupSystem {
}

trait ObpLookupSystem extends MdcLoggable {
var obpLookupSystem: ActorSystem = null
// @volatile + synchronized double-checked init: without it two threads can both see null,
// both build an ActorSystem (resource leak), and a reader can observe a stale null.
@volatile var obpLookupSystem: ActorSystem = null
val props_hostname = Helper.getHostname

def init (): ActorSystem = {
if (obpLookupSystem == null ) {
val system = ActorSystem("ObpLookupSystem", ConfigFactory.load(ConfigFactory.parseString(ObpActorConfig.lookupConf)))
logger.info(ObpActorConfig.lookupConf)
obpLookupSystem = system
if (obpLookupSystem == null) {
synchronized {
if (obpLookupSystem == null) {
val system = ActorSystem("ObpLookupSystem", ConfigFactory.load(ConfigFactory.parseString(ObpActorConfig.lookupConf)))
logger.info(ObpActorConfig.lookupConf)
obpLookupSystem = system
}
}
}
obpLookupSystem
}
Expand Down
Loading
Loading