Skip to content

Commit 25fd2cb

Browse files
authored
Merge pull request #986 from AbsaOSS/feature/spline-978-uuid-collision-handling
Feature/spline 978 UUID collision handling
2 parents 958b4c0 + b925f0a commit 25fd2cb

File tree

10 files changed

+115
-25
lines changed

10 files changed

+115
-25
lines changed

build/parent-pom/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
<java.version>1.8</java.version>
4141
<scala.version>2.12.13</scala.version>
4242
<scala.compat.version>2.12</scala.compat.version>
43-
<logback.version>1.2.3</logback.version>
43+
<logback.version>1.2.6</logback.version>
4444
<slf4j.version>1.7.25</slf4j.version>
4545
<json4s.version>3.6.7</json4s.version>
4646
<spring.version>5.2.2.RELEASE</spring.version>

persistence/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
<dependency>
6363
<groupId>com.arangodb</groupId>
6464
<artifactId>arangodb-java-driver</artifactId>
65-
<version>6.12.2</version>
65+
<version>6.14.0</version>
6666
</dependency>
6767
<dependency>
6868
<groupId>com.arangodb</groupId>

persistence/src/main/scala/za/co/absa/spline/persistence/model/entities.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ object DataSource {
109109
*/
110110
case class ExecutionPlan(
111111
name: Option[ExecutionPlan.Name],
112+
discriminator: Option[ExecutionPlan.Discriminator],
112113
systemInfo: Map[String, Any],
113114
agentInfo: Map[String, Any],
114115
extra: Map[String, Any],
@@ -117,6 +118,7 @@ case class ExecutionPlan(
117118

118119
object ExecutionPlan {
119120
type Name = String
121+
type Discriminator = String
120122
}
121123

122124
/**
@@ -127,6 +129,7 @@ object ExecutionPlan {
127129
case class Progress(
128130
timestamp: Long,
129131
durationNs: Option[Progress.JobDurationInNanos],
132+
discriminator: Option[ExecutionPlan.Discriminator],
130133
error: Option[Any],
131134
extra: Map[String, Any],
132135
override val _key: ArangoDocument.Key,

producer-model/src/main/scala/za/co/absa/spline/producer/model/v1_1/ExecutionEvent.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ package za.co.absa.spline.producer.model.v1_1
1818

1919
import za.co.absa.spline.producer.model.v1_1.ExecutionEvent._
2020

21-
import java.util.UUID
2221
import scala.language.implicitConversions
2322

2423
case class ExecutionEvent(
25-
planId: UUID,
24+
planId: ExecutionPlan.Id,
2625
timestamp: Long,
2726
durationNs: Option[DurationNs],
27+
discriminator: Option[ExecutionPlan.Discriminator] = None,
2828
error: Option[Any] = None,
2929
extra: Map[String, Any] = Map.empty
3030
)

producer-model/src/main/scala/za/co/absa/spline/producer/model/v1_1/executionPlan.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ import za.co.absa.spline.common.validation.{Constraint, ValidationUtils}
2121
import java.util.UUID
2222

2323
case class ExecutionPlan(
24-
id: UUID = UUID.randomUUID(),
24+
id: ExecutionPlan.Id = UUID.randomUUID(),
2525
name: Option[ExecutionPlan.Name],
26+
discriminator: Option[ExecutionPlan.Discriminator] = None,
2627

2728
operations: Operations,
2829
attributes: Seq[Attribute] = Nil,
@@ -45,8 +46,10 @@ case class ExecutionPlan(
4546
}
4647

4748
object ExecutionPlan {
49+
type Id = UUID
4850
type Name = String
4951
type DataSourceUri = String
52+
type Discriminator = String
5053
}
5154

5255
case class Operations(

producer-rest-core/src/main/scala/za/co/absa/spline/producer/rest/controller/ExecutionEventsController.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,20 @@ class ExecutionEventsController @Autowired()(
4848
{
4949
// Reference to the execution plan Id that was triggered
5050
planId: <UUID>,
51+
52+
// [Optional] A label that logically distinguish a group of one of multiple execution plans from another group.
53+
// If set, it has to match the discriminator of the associated execution plan.
54+
// The property is used for UUID collision detection.
55+
discriminator: <string>,
56+
5157
// Time (milliseconds since Epoch) when the execution finished
5258
timestamp: <number>,
59+
5360
// [Optional] Duration (in nanoseconds) of the execution
5461
durationNs: <number>,
5562
// [Optional] Additional info about the error (in case there was an error during the execution)
5663
error: {...},
64+
5765
// [Optional] Any other extra information related to the given execution event
5866
extra: {...}
5967
},

producer-rest-core/src/main/scala/za/co/absa/spline/producer/rest/controller/ExecutionPlansController.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ class ExecutionPlansController @Autowired()(
5252
// [Optional] A name of the application (script, job etc) that this execution plan represents.
5353
name: <string>,
5454
55+
// [Optional] A label that logically distinguish a group of one of multiple execution plans from another group.
56+
// If set, it has to match the discriminator of the associated execution events.
57+
// The property is used for UUID collision detection.
58+
discriminator: <string>,
59+
5560
// Operation level lineage info
5661
operations: {
5762
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright 2021 ABSA Group Limited
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package za.co.absa.spline.producer.service
18+
19+
import java.util.UUID
20+
21+
class UUIDCollisionDetectedException(
22+
entityName: String,
23+
id: UUID,
24+
discriminator: String
25+
) extends RuntimeException(s"$entityName UUID collision detected: $id, discriminator: $discriminator")

producer-services/src/main/scala/za/co/absa/spline/producer/service/model/ExecutionPlanPersistentModelBuilder.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ class ExecutionPlanPersistentModelBuilder private(
7878
def build(): ExecutionPlanPersistentModel = {
7979
val pmExecutionPlan = pm.ExecutionPlan(
8080
name = ep.name,
81+
discriminator = ep.discriminator,
8182
_key = ep.id.toString,
8283
systemInfo = ep.systemInfo.toJsonAs[Map[String, Any]],
8384
agentInfo = ep.agentInfo.map(_.toJsonAs[Map[String, Any]]).orNull,

producer-services/src/main/scala/za/co/absa/spline/producer/service/repo/ExecutionProducerRepositoryImpl.scala

Lines changed: 65 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ import za.co.absa.spline.persistence.tx.{ArangoTx, InsertQuery, TxBuilder}
2525
import za.co.absa.spline.persistence.{ArangoImplicits, Persister}
2626
import za.co.absa.spline.producer.model.v1_1.ExecutionEvent._
2727
import za.co.absa.spline.producer.model.{v1_1 => apiModel}
28-
import za.co.absa.spline.producer.service.InconsistentEntityException
2928
import za.co.absa.spline.producer.service.model.{ExecutionEventKeyCreator, ExecutionPlanPersistentModel, ExecutionPlanPersistentModelBuilder}
29+
import za.co.absa.spline.producer.service.{UUIDCollisionDetectedException, InconsistentEntityException}
3030

3131
import java.util.UUID
3232
import scala.compat.java8.FutureConverters._
@@ -42,15 +42,18 @@ class ExecutionProducerRepositoryImpl @Autowired()(db: ArangoDatabaseAsync) exte
4242
import ExecutionProducerRepositoryImpl._
4343

4444
override def insertExecutionPlan(executionPlan: apiModel.ExecutionPlan)(implicit ec: ExecutionContext): Future[Unit] = Persister.execute({
45-
val planAlreadyExistsFuture = db.queryOne[Boolean](
45+
// Here I have to use the type parameter `Any` and cast to `String` later due to ArangoDb Java driver issue.
46+
// See https://github.com/arangodb/arangodb-java-driver/issues/389
47+
val eventualMaybeExistingDiscriminatorOpt: Future[Option[String]] = db.queryOptional[Any](
4648
s"""
4749
|WITH ${NodeDef.ExecutionPlan.name}
4850
|FOR ex IN ${NodeDef.ExecutionPlan.name}
4951
| FILTER ex._key == @key
50-
| COLLECT WITH COUNT INTO cnt
51-
| RETURN TO_BOOL(cnt)
52+
| LIMIT 1
53+
| RETURN ex.discriminator
5254
| """.stripMargin,
53-
Map("key" -> executionPlan.id))
55+
Map("key" -> executionPlan.id)
56+
).map(_.map(Option(_).map(_.toString).orNull))
5457

5558
val eventualPersistedDSKeyByURI: Future[Map[DataSource.Uri, DataSource.Key]] = db.queryAs[DataSource](
5659
s"""
@@ -64,15 +67,21 @@ class ExecutionProducerRepositoryImpl @Autowired()(db: ArangoDatabaseAsync) exte
6467

6568
for {
6669
persistedDSKeyByURI <- eventualPersistedDSKeyByURI
67-
planAlreadyExists <- planAlreadyExistsFuture
68-
_ <-
69-
if (planAlreadyExists) Future.successful(Unit) // nothing more to do
70-
else createInsertTransaction(executionPlan, persistedDSKeyByURI).execute(db)
70+
maybeExistingDiscriminatorOpt <- eventualMaybeExistingDiscriminatorOpt
71+
_ <- maybeExistingDiscriminatorOpt match {
72+
case Some(existingDiscriminatorOrNull) =>
73+
// execution plan with the given ID already exists
74+
ensureNoExecPlanIDCollision(executionPlan.id, executionPlan.discriminator.orNull, existingDiscriminatorOrNull)
75+
Future.successful(Unit)
76+
case None =>
77+
// no execution plan with the given ID found
78+
createInsertTransaction(executionPlan, persistedDSKeyByURI).execute(db)
79+
}
7180
} yield Unit
7281
})
7382

7483
override def insertExecutionEvents(events: Array[apiModel.ExecutionEvent])(implicit ec: ExecutionContext): Future[Unit] = Persister.execute({
75-
val eventualExecPlanDetails = db.queryStream[ExecPlanDetails](
84+
val eventualExecPlanInfos: Future[Seq[ExecPlanInfo]] = db.queryStream[ExecPlanInfo](
7685
s"""
7786
|WITH executionPlan, executes, operation, dataSource
7887
|FOR ep IN executionPlan
@@ -82,21 +91,33 @@ class ExecutionProducerRepositoryImpl @Autowired()(db: ArangoDatabaseAsync) exte
8291
| LET ds = FIRST(FOR v IN 1 OUTBOUND ep affects RETURN v)
8392
|
8493
| RETURN {
85-
| "executionPlanKey" : ep._key,
86-
| "frameworkName" : CONCAT(ep.systemInfo.name, " ", ep.systemInfo.version),
87-
| "applicationName" : ep.name,
88-
| "dataSourceUri" : ds.uri,
89-
| "dataSourceName" : ds.name,
90-
| "dataSourceType" : wo.extra.destinationType,
91-
| "append" : wo.append
94+
| key : ep._key,
95+
| discriminator : ep.discriminator,
96+
| details: {
97+
| "executionPlanKey" : ep._key,
98+
| "frameworkName" : CONCAT(ep.systemInfo.name, " ", ep.systemInfo.version),
99+
| "applicationName" : ep.name,
100+
| "dataSourceUri" : ds.uri,
101+
| "dataSourceName" : ds.name,
102+
| "dataSourceType" : wo.extra.destinationType,
103+
| "append" : wo.append
104+
| }
92105
| }
93106
|""".stripMargin,
94107
Map("keys" -> events.map(_.planId))
95108
)
96109

97110
for {
98-
execPlansDetails <- eventualExecPlanDetails
99-
res <- createInsertTransaction(events, execPlansDetails.toArray).execute(db)
111+
execPlansInfos <- eventualExecPlanInfos
112+
(execPlanDiscrById, execPlansDetails) = execPlansInfos
113+
.foldLeft((Map.empty[apiModel.ExecutionPlan.Id, apiModel.ExecutionPlan.Discriminator], Vector.empty[ExecPlanDetails])) {
114+
case ((descrByIdAcc, detailsAcc), ExecPlanInfo(id, discr, details)) =>
115+
(descrByIdAcc + (UUID.fromString(id) -> discr), detailsAcc :+ details)
116+
}
117+
res <- {
118+
events.foreach(e => ensureNoExecPlanIDCollision(e.planId, e.discriminator.orNull, execPlanDiscrById(e.planId)))
119+
createInsertTransaction(events, execPlansDetails.toArray).execute(db)
120+
}
100121
} yield res
101122
})
102123

@@ -117,6 +138,13 @@ class ExecutionProducerRepositoryImpl @Autowired()(db: ArangoDatabaseAsync) exte
117138

118139
object ExecutionProducerRepositoryImpl {
119140

141+
case class ExecPlanInfo(
142+
key: ArangoDocument.Key,
143+
discriminator: ExecutionPlan.Discriminator,
144+
details: ExecPlanDetails) {
145+
def this() = this(null, null, null)
146+
}
147+
120148
private def createInsertTransaction(
121149
executionPlan: apiModel.ExecutionPlan,
122150
persistedDSKeyByURI: Map[DataSource.Uri, DataSource.Key]
@@ -175,7 +203,15 @@ object ExecutionProducerRepositoryImpl {
175203
.zip(execPlansDetails)
176204
.map { case (e, pd) =>
177205
val key = new ExecutionEventKeyCreator(e).executionEventKey
178-
Progress(e.timestamp, e.durationNs, e.error, e.extra, key, pd)
206+
Progress(
207+
timestamp = e.timestamp,
208+
durationNs = e.durationNs,
209+
discriminator = e.discriminator,
210+
error = e.error,
211+
extra = e.extra,
212+
_key = key,
213+
execPlanDetails = pd
214+
)
179215
}
180216

181217
val progressEdges = progressNodes
@@ -188,4 +224,13 @@ object ExecutionProducerRepositoryImpl {
188224
.buildTx
189225
}
190226

227+
private def ensureNoExecPlanIDCollision(
228+
planId: apiModel.ExecutionPlan.Id,
229+
actualDiscriminator: apiModel.ExecutionPlan.Discriminator,
230+
expectedDiscriminator: apiModel.ExecutionPlan.Discriminator
231+
): Unit = {
232+
if (actualDiscriminator != expectedDiscriminator) {
233+
throw new UUIDCollisionDetectedException("ExecutionPlan", planId, actualDiscriminator)
234+
}
235+
}
191236
}

0 commit comments

Comments
 (0)