diff --git a/obp-api/src/main/scala/code/api/util/migration/Migration.scala b/obp-api/src/main/scala/code/api/util/migration/Migration.scala index 26feb8bf43..c2d55a7eae 100644 --- a/obp-api/src/main/scala/code/api/util/migration/Migration.scala +++ b/obp-api/src/main/scala/code/api/util/migration/Migration.scala @@ -99,6 +99,7 @@ object Migration extends MdcLoggable { alterMappedCustomerAttribute(startedBeforeSchemifier) dropMappedBadLoginAttemptIndex() alterMetricColumnUrlLength() + alterMetricArchiveColumnCorrelationidLength() // populateViewDefinitionCanAddTransactionRequestToBeneficiary() // populateViewDefinitionCanSeeTransactionStatus() alterCounterpartyLimitFieldType() @@ -493,6 +494,13 @@ object Migration extends MdcLoggable { } } + private def alterMetricArchiveColumnCorrelationidLength(): Boolean = { + val name = nameOf(alterMetricArchiveColumnCorrelationidLength) + runOnce(name) { + MigrationOfMetricArchiveTable.alterColumnCorrelationidLength(name) + } + } + private def dropConsentAuthContextDropIndex(): Boolean = { val name = nameOf(dropConsentAuthContextDropIndex) runOnce(name) { diff --git a/obp-api/src/main/scala/code/api/util/migration/MigrationOfMetricArchiveTable.scala b/obp-api/src/main/scala/code/api/util/migration/MigrationOfMetricArchiveTable.scala new file mode 100644 index 0000000000..f96ff069a7 --- /dev/null +++ b/obp-api/src/main/scala/code/api/util/migration/MigrationOfMetricArchiveTable.scala @@ -0,0 +1,76 @@ +package code.api.util.migration + +import java.time.format.DateTimeFormatter +import java.time.{ZoneId, ZonedDateTime} + +import code.api.util.APIUtil +import code.api.util.migration.Migration.{DbFunction, saveLog} +import code.metrics.MetricArchive +import net.liftweb.common.Full +import net.liftweb.mapper.Schemifier + +/** + * Widen `metricarchive.correlationid` from varchar(36) to varchar(256) so it matches + * the live `metric` table (which was already widened by + * [[MigrationOfMetricTable.alterColumnCorrelationidLength]]). + * + * Why this is needed: the correlation id is whatever the caller sends in `X-Request-ID` + * (mandatory for Berlin Group / PSD2, optional elsewhere) — a free-form, client-controlled + * string up to 256 chars, not a UUID. The archive column was modelled as a MappedUUID + * (varchar 36), so the MetricsArchiveScheduler failed on the first row whose correlation id + * exceeded 36 chars with `value too long for type character varying(36)`. Because the + * archiver moves rows oldest-first, the same un-archivable rows were retried every run, so + * no archive run ever succeeded. Lift's Schemifier never alters an existing column's width, + * so this explicit migration is required on already-provisioned databases. + */ +object MigrationOfMetricArchiveTable { + + val oneDayAgo = ZonedDateTime.now(ZoneId.of("UTC")).minusDays(1) + val oneYearInFuture = ZonedDateTime.now(ZoneId.of("UTC")).plusYears(1) + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm'Z'") + + def alterColumnCorrelationidLength(name: String): Boolean = { + DbFunction.tableExists(MetricArchive) + match { + case true => + val startDate = System.currentTimeMillis() + val commitId: String = APIUtil.gitCommit + var isSuccessful = false + + val executedSql = + DbFunction.maybeWrite(true, Schemifier.infoF _) { + APIUtil.getPropsValue("db.driver") match { + case Full(dbDriver) if dbDriver.contains("com.microsoft.sqlserver.jdbc.SQLServerDriver") => + () => + """ + |ALTER TABLE metricarchive ALTER COLUMN correlationid varchar(256); + |""".stripMargin + case _ => + () => + """ + |ALTER TABLE metricarchive ALTER COLUMN correlationid TYPE character varying(256); + |""".stripMargin + } + } + + val endDate = System.currentTimeMillis() + val comment: String = + s"""Executed SQL: + |$executedSql + |""".stripMargin + isSuccessful = true + saveLog(name, commitId, isSuccessful, startDate, endDate, comment) + isSuccessful + + case false => + val startDate = System.currentTimeMillis() + val commitId: String = APIUtil.gitCommit + val isSuccessful = false + val endDate = System.currentTimeMillis() + val comment: String = + s"""${MetricArchive._dbTableNameLC} table does not exist""".stripMargin + saveLog(name, commitId, isSuccessful, startDate, endDate, comment) + isSuccessful + } + } +} diff --git a/obp-api/src/main/scala/code/metrics/MappedMetrics.scala b/obp-api/src/main/scala/code/metrics/MappedMetrics.scala index 9fe6bac89c..cf1d8c0524 100644 --- a/obp-api/src/main/scala/code/metrics/MappedMetrics.scala +++ b/obp-api/src/main/scala/code/metrics/MappedMetrics.scala @@ -658,6 +658,12 @@ class MappedMetric extends APIMetric with LongKeyedMapper[MappedMetric] with IdP //(GET, POST etc.) --S.request.get.requestType object verb extends MappedString(this, 16) object httpCode extends MappedInt(this) + // NOT necessarily a UUID, despite the generateUUID default. When the caller sends + // an `X-Request-ID` header (mandatory for Berlin Group / PSD2, optional elsewhere) + // that value is adopted verbatim as the correlation id (see APIUtil.scala ~2959), + // echoed back as the `Correlation-Id` response header. It is client-controlled and + // free-form on non-Berlin-Group paths, hence the generous 256 width. The archive + // copy (MetricArchive.correlationId) MUST keep the same width or the archiver fails. object correlationId extends MappedString(this, 256) { override def dbNotNull_? = true override def defaultValue = generateUUID() @@ -725,7 +731,13 @@ class MetricArchive extends APIMetric with LongKeyedMapper[MetricArchive] with I //(GET, POST etc.) --S.request.get.requestType object verb extends MappedString(this, 16) object httpCode extends MappedInt(this) - object correlationId extends MappedUUID(this){ + // Must mirror the source Metric.correlationId width (256), NOT a UUID's 36. The + // live correlation id is a free string (client-supplied or upstream trace id) that + // routinely exceeds 36 chars; as a MappedUUID (varchar 36) this column rejected the + // first such row the archiver copied with "value too long for type character + // varying(36)", failing every run — and since the archiver moves oldest-first, the + // same un-archivable rows were retried forever, so no run ever succeeded. + object correlationId extends MappedString(this, 256){ override def dbNotNull_? = true } object responseBody extends MappedText(this)