Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
e686781
fix: a sandbox IBAN identifies one account, so no two may share one
hongwei1 Aug 25, 2026
4a5e320
test: sweep every endpoint for the auth it declares, and for crashes
hongwei1 Aug 25, 2026
4d8a145
test: pin the two contracts a serialization upgrade changes silently
hongwei1 Aug 25, 2026
2a4d6fb
test: a cancelled check reports as a pass, so stop cancelling in CI
hongwei1 Aug 25, 2026
4ec3c92
refactor: reduce cognitive complexity of StarConnector invocation han…
hongwei1 Aug 25, 2026
e5ff924
fix: namespace the cache by serialization identity, so one version ca…
hongwei1 Aug 25, 2026
d2849ef
feat: apply idempotency to every route tree, not only v7.0.0
hongwei1 Aug 25, 2026
bb77c32
fix: make six endpoints declare the authentication they actually enforce
hongwei1 Aug 25, 2026
dbb94da
test: compare the class Kryo decodes into, not just the value
hongwei1 Aug 25, 2026
3d2f10c
test: make DynamicUtilTest drive the production code, and let CI refu…
hongwei1 Aug 25, 2026
68e9972
fix: stop reporting six client-side conditions as server faults
hongwei1 Aug 26, 2026
dcd5b32
test: fix two ways the sweep reported the wrong thing, and record the…
hongwei1 Aug 26, 2026
1edd5bd
test: count skips from what the XML actually contains, not an absent …
hongwei1 Aug 26, 2026
cea54ff
fix: drop CallContext from the endpoint-mapping and FX rate cache keys
hongwei1 Aug 16, 2026
65561a3
fix: scope the idempotency key by operation, not just consumer
hongwei1 Aug 26, 2026
5b98bd8
fix: don't report a DB failure during view lookup as 404
hongwei1 Aug 26, 2026
4f11809
fix: don't report a broken security provider as an unsigned JWT
hongwei1 Aug 26, 2026
b999a26
fix: report a missing portal URL prop as 503, not 400
hongwei1 Aug 26, 2026
277bddc
test: make the failure/auth sweep drift check able to fail
hongwei1 Aug 26, 2026
1129d0a
fix: only run idempotency lock/response logic at the tier that matched
hongwei1 Aug 26, 2026
2a9b87d
fix: recognize PAYMENT_SERVICE, PAYMENT_PRODUCT and SCA_METHOD as swe…
hongwei1 Aug 26, 2026
8e27cc8
fix: close the write-then-invalidate race on the endpoint-mapping cache
hongwei1 Aug 26, 2026
705e9e5
fix: correct the zero-test floor diagnostic to quote the threshold it…
hongwei1 Aug 26, 2026
9a2d841
test: factor the duplicated sweep-fixture logic into one shared trait
hongwei1 Aug 26, 2026
0f1984c
refactor: extract duplicated literals in CacheSerializationNamespaceTest
hongwei1 Aug 26, 2026
5d1f103
refactor: extract duplicated literals in IdempotencyMiddlewareTest
hongwei1 Aug 26, 2026
7d9e2e7
refactor: extract duplicated literal in RedisTestTargetTest
hongwei1 Aug 26, 2026
6ad14de
fix: include Berlin Group v1.3 alias docs in the global resource-doc …
hongwei1 Aug 31, 2026
f1e9f4e
test: cover Berlin Group v1.3 alias in resource-doc registry parity test
hongwei1 Aug 31, 2026
f1344c1
refactor: remove dead activeResourceDocs identity match in resource-d…
hongwei1 Aug 31, 2026
215ca05
feat: add Java language support to DynamicResourceDoc
hongwei1 Aug 31, 2026
69eaa44
fix: thread programming_lang into dynamic-resource-doc validate endpoint
hongwei1 Aug 31, 2026
4b469ef
refactor: derive resource-doc dispatch and the global operation-id un…
hongwei1 Aug 31, 2026
5af4cb6
test: pin BGv2-getAccountDetails as a resolvable api-collection-endpo…
hongwei1 Aug 31, 2026
2b7a288
Merge branch 'develop-obp' into feature/dynamicresourcedoc-java-support
hongwei1 Aug 31, 2026
75dff04
test: extract duplicated dynamic-resource-docs request literal to a h…
hongwei1 Aug 31, 2026
cd1b548
test: extract shared 401/403/200 role-gate assertion for dynamic reso…
hongwei1 Aug 31, 2026
3ec0b99
test: enable the Berlin Group v1.3 alias by default so it gets HTTP-l…
hongwei1 Aug 31, 2026
2a754c7
test: regenerate frozen_type_meta_data.txt to match the updated blob
hongwei1 Aug 31, 2026
ff17b5e
test: pin a v7-only operation id as resolvable, covering the third dr…
hongwei1 Aug 31, 2026
3a50efd
fix: make Java dynamic-code dependency validation actually enforce
hongwei1 Aug 31, 2026
d252dc5
fix: build the global union from the current OBP surface only, in a d…
hongwei1 Aug 31, 2026
6ce2669
test: cover Java dynamic-code dependency validation end-to-end
hongwei1 Aug 31, 2026
39c131a
fix: treat an unset berlin_group_v1_3_alias_path as no alias at all
hongwei1 Aug 31, 2026
3125596
fix: give the registry a tie-free order and a guard that cannot be by…
hongwei1 Aug 31, 2026
0318885
Added auth_type metrics column
simonredfern Sep 1, 2026
9b458b6
fix: don't let a cached compile result skip fresh dependency validation
hongwei1 Sep 1, 2026
45a965a
fix: restore Berlin Group precedence and stop registering an empty AP…
hongwei1 Sep 1, 2026
a369957
fix: satisfy CI's test-isolation lint for the setPropsValues helper
hongwei1 Sep 1, 2026
da5a64b
Fix to Account Application v3.1.0
simonredfern Sep 1, 2026
b7bef71
fix: decouple dependency-whitelist validation from show_used_connecto…
hongwei1 Sep 1, 2026
eee46ac
fix: register compiled Java bytecode with ClassPool only once per source
hongwei1 Sep 1, 2026
04b1417
fix: fall back to Scala for dynamic resource docs with a NULL lang co…
hongwei1 Sep 1, 2026
9b238b0
fix: rank the Berlin Group v1.3 alias by identity, not by its configu…
hongwei1 Sep 1, 2026
759cc82
fix: strict dependency validation unconditionally rejected every Java…
hongwei1 Sep 1, 2026
4477e51
Merge pull request #2903 from simonredfern/develop
simonredfern Sep 1, 2026
ebf085f
Merge pull request #2902 from hongwei1/feature/bg-v13-alias-resource-…
simonredfern Sep 1, 2026
3276ffd
Merge remote-tracking branch 'origin/develop' into feature/dynamicres…
hongwei1 Sep 1, 2026
a98ba98
Merge pull request #2895 from hongwei1/test/endpoint-sweep-and-cache-…
simonredfern Sep 1, 2026
84ea41a
Merge remote-tracking branch 'origin/develop' into feature/dynamicres…
hongwei1 Sep 1, 2026
adea5e5
fix: recurse through multi-level same-class calls in Java dependency …
hongwei1 Sep 1, 2026
b745291
fix: reject unsupported programming_lang in the v6 validate endpoint
hongwei1 Sep 1, 2026
0fbd2f5
fix: log a whitelist rejection distinctly from a genuine compile failure
hongwei1 Sep 1, 2026
7b6057f
perf: bind allowedCompilationMethods once per validateDependency call
hongwei1 Sep 1, 2026
6bb5223
fix: fail fast when a compiled Java class's bytecode is missing
hongwei1 Sep 1, 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
7 changes: 7 additions & 0 deletions .github/workflows/build_container.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ jobs:
# --------------------------------------------------------------------------
compile:
runs-on: ubuntu-latest
# The test job has carried a timeout since it was written; compile and report never did,
# so a hung Maven resolve blocks the build until GitHub's own 6-hour ceiling.
timeout-minutes: 25
steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -289,6 +292,9 @@ jobs:
echo ResetPasswordUrlEnabled=true >> obp-api/src/main/resources/props/test.default.props
echo consents.allowed=true >> obp-api/src/main/resources/props/test.default.props
echo hikari.maximumPoolSize=20 >> obp-api/src/main/resources/props/test.default.props
# Enables the Berlin Group v1.3 alias so ResourceDocRegistryParityTest and
# ApiCollectionEndpointTest can exercise a real alias operation id end to end.
echo berlin_group_v1_3_alias_path=0.6/v1 >> obp-api/src/main/resources/props/test.default.props
echo write_metrics=false >> obp-api/src/main/resources/props/test.default.props
# Log emails instead of opening a real SMTP socket: without this,
# LocalMappedConnector.sendCustomerNotification's EMAIL branch calls
Expand Down Expand Up @@ -460,6 +466,7 @@ jobs:
needs: test
runs-on: ubuntu-latest
if: always()
timeout-minutes: 10
steps:
- uses: actions/checkout@v4

Expand Down
18 changes: 18 additions & 0 deletions .github/workflows/build_pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ jobs:
# --------------------------------------------------------------------------
compile:
runs-on: ubuntu-latest
# The test job has carried a timeout since it was written; compile and report never did,
# so a hung Maven resolve blocks the build until GitHub's own 6-hour ceiling.
timeout-minutes: 25
steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -283,6 +286,9 @@ jobs:
echo ResetPasswordUrlEnabled=true >> obp-api/src/main/resources/props/test.default.props
echo consents.allowed=true >> obp-api/src/main/resources/props/test.default.props
echo hikari.maximumPoolSize=20 >> obp-api/src/main/resources/props/test.default.props
# Enables the Berlin Group v1.3 alias so ResourceDocRegistryParityTest and
# ApiCollectionEndpointTest can exercise a real alias operation id end to end.
echo berlin_group_v1_3_alias_path=0.6/v1 >> obp-api/src/main/resources/props/test.default.props
echo write_metrics=false >> obp-api/src/main/resources/props/test.default.props
# Log emails instead of opening a real SMTP socket: without this,
# LocalMappedConnector.sendCustomerNotification's EMAIL branch calls
Expand All @@ -302,6 +308,17 @@ jobs:
echo allow_user_generated_scala_code=true >> obp-api/src/main/resources/props/test.default.props

- name: Run tests — shard ${{ matrix.shard }} (${{ matrix.name }})
env:
# This job has declared a redis service since it was written, but nothing ever failed
# when the service was absent: ConcurrentRateLimiterRaceTest and
# MethodRoutingCacheInvalidationTest each `assume` a reachable Redis and cancel
# otherwise, and a cancelled test reports as a pass. Dropping the services: block, or
# a container that never became healthy, would have taken the rate-limiter and
# cache-invalidation races out of the run without changing a single report.
#
# RedisTestTarget turns that cancellation into a failure wherever this is set.
# Developers leave it unset and keep the skip.
OBP_TEST_REDIS_REQUIRED: "true"
run: |
# wildcardSuites requires comma-separated package prefixes (-w per entry).
# The YAML >- scalar collapses newlines to spaces, so we convert here.
Expand Down Expand Up @@ -453,6 +470,7 @@ jobs:
needs: test
runs-on: ubuntu-latest
if: always()
timeout-minutes: 10
steps:
- uses: actions/checkout@v4

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package code.api.ResourceDocs1_4_0
import code.api.Constant.{GET_DYNAMIC_RESOURCE_DOCS_TTL, GET_STATIC_RESOURCE_DOCS_TTL, HostName, PARAM_LOCALE}
import code.api.OBPRestHelper
import code.api.cache.Caching
import code.api.dynamic.endpoint.OBPAPIDynamicEndpoint
import code.api.dynamic.entity.OBPAPIDynamicEntity
import code.api.util.APIUtil._
import code.api.util.ApiRole.{canReadDynamicResourceDocsAtOneBank, canReadResourceDoc}
import code.api.util.ApiTag._
Expand All @@ -22,7 +20,6 @@ import code.api.v4_0_0.{APIMethods400, OBPAPI4_0_0}
import code.api.v5_0_0.OBPAPI5_0_0
import code.api.v5_1_0.OBPAPI5_1_0
import code.api.v6_0_0.OBPAPI6_0_0
import code.api.berlin.group.ConstantsBG
import code.apicollectionendpoint.MappedApiCollectionEndpointsProvider
import code.util.Helper
import code.util.Helper.{MdcLoggable, ObpS, SILENCE_IS_GOLDEN}
Expand Down Expand Up @@ -331,59 +328,15 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth

logger.debug(s"getResourceDocsList says requestedApiVersion is $requestedApiVersion")

val resourceDocs = requestedApiVersion match {
case ApiVersion.v7_0_0 => code.api.v7_0_0.Http4s700.allResourceDocs // Use aggregated docs for v7.0.0
case ConstantsBG.`berlinGroupVersion1` => code.api.berlin.group.v1_3.Http4sBGv13.resourceDocs
case ConstantsBG.`berlinGroupVersion2` => code.api.berlin.group.v2.Http4sBGv2.resourceDocs
case ApiVersion.v6_0_0 => code.api.util.http4s.Http4sResourceDocAggregation.v600
case ApiVersion.v5_1_0 => code.api.util.http4s.Http4sResourceDocAggregation.v510
case ApiVersion.v5_0_0 => code.api.util.http4s.Http4sResourceDocAggregation.v500
case ApiVersion.v4_0_0 => code.api.util.http4s.Http4sResourceDocAggregation.v400
case ApiVersion.v3_1_0 => code.api.util.http4s.Http4sResourceDocAggregation.v310
case ApiVersion.v3_0_0 => code.api.util.http4s.Http4sResourceDocAggregation.v300
case ApiVersion.v2_2_0 => code.api.util.http4s.Http4sResourceDocAggregation.v220
case ApiVersion.v2_1_0 => code.api.util.http4s.Http4sResourceDocAggregation.v210
case ApiVersion.v2_0_0 => code.api.util.http4s.Http4sResourceDocAggregation.v200
case ApiVersion.v1_4_0 => code.api.util.http4s.Http4sResourceDocAggregation.v140
case ApiVersion.v1_3_0 => code.api.util.http4s.Http4sResourceDocAggregation.v130
case ApiVersion.v1_2_1 => code.api.util.http4s.Http4sResourceDocAggregation.v121
case ApiVersion.`dynamic-endpoint` => OBPAPIDynamicEndpoint.allResourceDocs
case ApiVersion.`dynamic-entity` => OBPAPIDynamicEntity.allResourceDocs
case version: ScannedApiVersion => ScannedApis.versionMapScannedApis.get(version).map(_.allResourceDocs).getOrElse(ArrayBuffer.empty[ResourceDoc])
case _ => ArrayBuffer.empty[ResourceDoc]
}
// ResourceDocRegistry is the single source of truth for both this per-version dispatch and
// APIUtil.allStaticResourceDocs' global operation-id union -- see that object's doc comment.
val resourceDocs = ResourceDocRegistry.docsFor(requestedApiVersion)

logger.debug(s"There are ${resourceDocs.length} resource docs available to $requestedApiVersion")

val activeResourceDocs = requestedApiVersion match {
case ApiVersion.v7_0_0 => resourceDocs
case ConstantsBG.`berlinGroupVersion1` => resourceDocs // fully on http4s — no Lift route filter
case ConstantsBG.`berlinGroupVersion2` => resourceDocs
case ApiVersion.v1_2_1 => resourceDocs
case ApiVersion.v6_0_0 => resourceDocs // fully on http4s — no Lift route filter
case ApiVersion.v5_1_0 => resourceDocs // fully on http4s — no Lift route filter
case ApiVersion.v5_0_0 => resourceDocs // fully on http4s — no Lift route filter
case ApiVersion.v4_0_0 => resourceDocs // fully on http4s — no Lift route filter
case ApiVersion.v3_1_0 => resourceDocs // fully on http4s — no Lift route filter
case ApiVersion.v3_0_0 => resourceDocs // fully on http4s — no Lift route filter
case ApiVersion.v2_2_0 => resourceDocs // fully on http4s — no Lift route filter
case ApiVersion.v2_1_0 => resourceDocs // fully on http4s — no Lift route filter
case ApiVersion.v2_0_0 => resourceDocs // fully on http4s — no Lift route filter
case ApiVersion.v1_4_0 => resourceDocs // fully on http4s — no Lift route filter
case ApiVersion.v1_3_0 => resourceDocs // fully on http4s — no Lift route filter
case ApiVersion.`dynamic-entity` => resourceDocs // runtime CRUD now on Http4sDynamicEntity; routes are Nil, skip Lift-route filter
case ApiVersion.`dynamic-endpoint` => resourceDocs // dispatch now on Http4sDynamicEndpoint (proxy + native Piece C); routes carry only the stub, skip Lift-route filter
case ApiVersion.ukOpenBankingV20 => resourceDocs // fully on http4s — no Lift route filter
case ApiVersion.ukOpenBankingV31 => resourceDocs // fully on http4s — no Lift route filter
case _ => resourceDocs
}

logger.debug(s"There are ${activeResourceDocs.length} resource docs available to $requestedApiVersion")


val activePlusLocalResourceDocs = ArrayBuffer[ResourceDoc]()

activePlusLocalResourceDocs ++= activeResourceDocs
activePlusLocalResourceDocs ++= resourceDocs
requestedApiVersion match
{
// only `obp` standard show the `localResourceDocs`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3202,6 +3202,7 @@ object SwaggerDefinitionsJSON {
operation_id = "OBPv4.0.0-getBanks",
api_instance_id = "obp_node_a",
consent_reference_id = Some(ExampleValue.consentReferenceIdExample.value),
auth_type = Some("Consent"),
certificate_trust = Some("forwarded"),
certificate_trust_detail = Some("cn=nginx-prod-1,ou=edge,o=tesobe gmbh,c=de")
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import cats.data.{Kleisli, OptionT}
import cats.effect._
import code.api.util.APIUtil.ResourceDoc
import code.api.util.http4s.ResourceDocMiddleware
import code.api.util.http4s.IdempotencyMiddleware
import code.util.Helper.MdcLoggable
import com.openbankproject.commons.util.ApiVersion
import org.http4s._
Expand Down Expand Up @@ -33,5 +34,5 @@ object Http4sUKOBv200 extends MdcLoggable {
Http4sUKOBv200AIS.routes(req)
}

val wrappedRoutes: HttpRoutes[IO] = ResourceDocMiddleware.apply(resourceDocs)(allRoutes)
val wrappedRoutes: HttpRoutes[IO] = ResourceDocMiddleware.apply(resourceDocs)(IdempotencyMiddleware(allRoutes))
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import cats.data.{Kleisli, OptionT}
import cats.effect._
import code.api.util.APIUtil.ResourceDoc
import code.api.util.http4s.ResourceDocMiddleware
import code.api.util.http4s.IdempotencyMiddleware
import code.util.Helper.MdcLoggable
import com.openbankproject.commons.util.ApiVersion
import org.http4s._
Expand Down Expand Up @@ -74,5 +75,5 @@ object Http4sUKOBv310 extends MdcLoggable {
.orElse(Http4sUKOBv310InternationalStandingOrders.routes(req))
}

val wrappedRoutes: HttpRoutes[IO] = ResourceDocMiddleware.apply(resourceDocs)(allRoutes)
val wrappedRoutes: HttpRoutes[IO] = ResourceDocMiddleware.apply(resourceDocs)(IdempotencyMiddleware(allRoutes))
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import cats.effect._
import code.api.util.APIUtil
import code.api.util.APIUtil.ResourceDoc
import code.api.util.http4s.ResourceDocMiddleware
import code.api.util.http4s.IdempotencyMiddleware
import code.util.Helper.MdcLoggable
import com.openbankproject.commons.util.ApiVersion
import org.http4s._
Expand Down Expand Up @@ -62,5 +63,5 @@ object Http4sUKOBv401 extends MdcLoggable {
routes(req).map(_.putHeaders(Header.Raw(fapiInteractionIdHeader, interactionId)))
}

val wrappedRoutes: HttpRoutes[IO] = withFapiInteractionId(ResourceDocMiddleware.apply(resourceDocs)(allRoutes))
val wrappedRoutes: HttpRoutes[IO] = withFapiInteractionId(ResourceDocMiddleware.apply(resourceDocs)(IdempotencyMiddleware(allRoutes)))
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import cats.effect._
import code.api.berlin.group.ConstantsBG
import code.api.util.APIUtil.ResourceDoc
import code.api.util.http4s.ResourceDocMiddleware
import code.api.util.http4s.IdempotencyMiddleware
import code.util.Helper.MdcLoggable
import org.http4s._

Expand Down Expand Up @@ -35,5 +36,5 @@ object Http4sBGv13 extends MdcLoggable {
.orElse(Http4sBGv13SigningBaskets.routes(req))
}

val wrappedRoutes: HttpRoutes[IO] = ResourceDocMiddleware.apply(resourceDocs)(allRoutes)
val wrappedRoutes: HttpRoutes[IO] = ResourceDocMiddleware.apply(resourceDocs)(IdempotencyMiddleware(allRoutes))
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,22 @@ import scala.collection.mutable.ArrayBuffer
*/
object OBP_BERLIN_GROUP_1_3_Alias extends OBPRestHelper with MdcLoggable with ScannedApis {

/**
* The version this aggregator registers under.
*
* `berlinGroupV13AliasPath` is empty when `berlin_group_v1_3_alias_path` is unset, so `.head` /
* `.last` must be guarded: this object is instantiated by the ScannedApis classpath scan, which
* catches a throwing companion and merely logs a warning, so an unguarded NoSuchElementException
* would drop the alias silently. Inactive registrations keep the empty-string version they have
* always had, which no request can address and which deliberately does NOT equal
* ConstantsBG.berlinGroupVersion1 -- colliding with the canonical BG v1.3 key would let this
* (doc-less) object win ScannedApis' `.toMap` and blank out /resource-docs/BGv1.3/obp.
*/
override val apiVersion: ScannedApiVersion =
ScannedApiVersion(berlinGroupV13AliasPath.head, berlinGroupV13AliasPath.head, berlinGroupV13AliasPath.last)
if (berlinGroupV13AliasPath.nonEmpty)
ScannedApiVersion(berlinGroupV13AliasPath.head, berlinGroupV13AliasPath.head, berlinGroupV13AliasPath.last)
else
ScannedApiVersion("", "", "")

val versionStatus: String = ApiVersionStatus.DRAFT.toString

Expand Down
13 changes: 11 additions & 2 deletions obp-api/src/main/scala/code/api/berlin/group/v2/Http4sBGv2.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,37 @@ import cats.data.{Kleisli, OptionT}
import cats.effect._
import code.api.berlin.group.ConstantsBG
import code.api.util.APIUtil.ResourceDoc
import code.api.util.ScannedApis
import code.api.util.http4s.ResourceDocMiddleware
import code.api.util.http4s.IdempotencyMiddleware
import code.util.Helper.MdcLoggable
import com.openbankproject.commons.util.ScannedApiVersion
import org.http4s._

import scala.collection.mutable.ArrayBuffer

object Http4sBGv2 extends MdcLoggable {
object Http4sBGv2 extends MdcLoggable with ScannedApis {

type HttpF[A] = OptionT[IO, A]

val implementedInApiVersion = ConstantsBG.berlinGroupVersion2

// ScannedApis discovery marker: makes BGv2 convention-driven like the other Berlin Group /
// UK Open Banking standards, so ResourceDocRegistry picks it up without a hand-maintained entry.
override val apiVersion: ScannedApiVersion = implementedInApiVersion

val resourceDocs: ArrayBuffer[ResourceDoc] =
Http4sBGv2AIS.resourceDocs ++
Http4sBGv2PIS.resourceDocs ++
Http4sBGv2PIIS.resourceDocs

override val allResourceDocs: ArrayBuffer[ResourceDoc] = resourceDocs

val allRoutes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req =>
Http4sBGv2AIS.routes(req)
.orElse(Http4sBGv2PIS.routes(req))
.orElse(Http4sBGv2PIIS.routes(req))
}

val wrappedRoutes: HttpRoutes[IO] = ResourceDocMiddleware.apply(resourceDocs)(allRoutes)
val wrappedRoutes: HttpRoutes[IO] = ResourceDocMiddleware.apply(resourceDocs)(IdempotencyMiddleware(allRoutes))
}
46 changes: 45 additions & 1 deletion obp-api/src/main/scala/code/api/cache/Redis.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import code.util.Helper.MdcLoggable
import com.openbankproject.commons.ExecutionContext.Implicits.global
import redis.clients.jedis.{Jedis, JedisPool, JedisPoolConfig}
import scalacache.memoization.{cacheKeyExclude, memoizeF, memoizeSync}
import scalacache.{Cache, Flags}
import scalacache.{Cache, CacheConfig, DefaultCacheKeyBuilder, Flags}
import scalacache.redis.RedisCache
import scalacache.serialization.{Codec, FailedToDecode}
import redis.clients.jedis.{Jedis, JedisPool, JedisPoolConfig}
Expand Down Expand Up @@ -321,6 +321,50 @@ object Redis extends MdcLoggable {
// building one per call only put two allocations in front of every cache read on the request
// path. RedisCache is a thin wrapper over the pool built above and opens nothing of its own, so
// the pool, its authentication and its SSL configuration stay shared.
/**
* The serialization identity these cached bytes were produced under.
*
* Cache entries are Kryo-encoded, and what Kryo produces depends on the Scala library and the
* chill build that encoded it. Two OBP-API versions compiled against different ones therefore
* write mutually unreadable bytes into the same keys -- and "unreadable" is the optimistic
* case. Measured across the 2.12 -> 2.13 migration: an EMPTY `List`, written by chill 0.9.3,
* decodes under 0.9.5 into a `scala.collection.immutable.Queue`. That decode SUCCEEDS. It is
* only at the call site, whose signature says `List`, that it fails --
*
* class scala.collection.immutable.Queue cannot be cast to
* class scala.collection.immutable.List
*
* -- so the caller gets a 500 rather than a cache miss, and gets it for the whole TTL, because
* a failed read does not evict the entry. Reproduced on `GET /management/dynamic-message-docs`
* and `GET /management/connector-methods`: 200 on 2.12, 500 on 2.13 reading 2.12's entry, and
* fine in either version on its own. That is a rolling upgrade, or any upgrade against a warm
* Redis.
*
* The migration note anticipated the risk and described the consequence as a cold cache. For
* values that fail to decode that is exactly right. This handles the ones that do not fail.
*
* Namespacing the key is the fix rather than casting defensively at each call site: there are
* eight `List`-returning memoized methods today, the same drift can hit any other type, and no
* amount of care at the call sites can make bytes already in Redis readable. Entries written by
* another version simply stop being addressable and age out on their own TTL.
*
* The Scala binary version is the axis that moved here and is the one derived automatically.
* `obp.cache.serialization.version` is for the case it does not cover -- a dependency upgrade
* that changes the encoding without changing the Scala version, which is what chill 0.9.3 to
* 0.9.5 would have been on its own. Bump it in that situation; the cost is one cold cache.
*/
private val serializationNamespace: String = {
val scalaBinary = scala.util.Properties.versionNumberString.split('.').take(2).mkString(".")
val manual = APIUtil.getPropsValue("obp.cache.serialization.version", "1")
s"obpser$manual-scala$scalaBinary"
}

// Prefixing happens here, in the key builder, rather than at the call sites: scalacache derives
// the rest of the key from the enclosing method and its arguments, and every caller goes through
// it. `memoizeSync` and `memoizeF` both read this same implicit config.
implicit val cacheConfig: CacheConfig =
CacheConfig(cacheKeyBuilder = DefaultCacheKeyBuilder(keyPrefix = Some(serializationNamespace)))

private val sharedCache: Cache[Any] = RedisCache[Any](jedisPool)
private def cacheFor[A]: Cache[A] = sharedCache.asInstanceOf[Cache[A]]

Expand Down
Loading
Loading