Skip to content

Commit 4c904da

Browse files
authored
Feature/348 create checkpoint v2 (#407)
1 parent 6387a32 commit 4c904da

33 files changed

Lines changed: 423 additions & 228 deletions

File tree

‎.github/workflows/build.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ jobs:
5252
postgres:
5353
image: postgres:15
5454
env:
55-
POSTGRES_PASSWORD: postgres
55+
POSTGRES_PASSWORD: changeme
5656
POSTGRES_DB: atum_db
5757
options: >-
5858
--health-cmd pg_isready

‎agent/src/main/scala/za/co/absa/atum/agent/dispatcher/ConsoleDispatcher.scala‎

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,7 @@ package za.co.absa.atum.agent.dispatcher
1818

1919
import com.typesafe.config.Config
2020
import org.apache.spark.internal.Logging
21-
import za.co.absa.atum.model.dto.{
22-
AdditionalDataDTO,
23-
AdditionalDataItemDTO,
24-
AdditionalDataPatchDTO,
25-
AtumContextDTO,
26-
CheckpointDTO,
27-
PartitioningDTO,
28-
PartitioningSubmitDTO
29-
}
21+
import za.co.absa.atum.model.dto._
3022

3123
/**
3224
* dispatcher useful for development, testing and debugging

‎agent/src/main/scala/za/co/absa/atum/agent/dispatcher/Dispatcher.scala‎

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,7 @@
1717
package za.co.absa.atum.agent.dispatcher
1818

1919
import com.typesafe.config.Config
20-
import za.co.absa.atum.model.dto.{
21-
AdditionalDataDTO,
22-
AdditionalDataPatchDTO,
23-
AtumContextDTO,
24-
CheckpointDTO,
25-
PartitioningDTO,
26-
PartitioningSubmitDTO
27-
}
20+
import za.co.absa.atum.model.dto._
2821

2922
/**
3023
* This class provides a contract for different dispatchers. It has a constructor foe eventual creation via reflection.

‎agent/src/main/scala/za/co/absa/atum/agent/dispatcher/HttpDispatcher.scala‎

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import sttp.capabilities
2222
import sttp.client3._
2323
import sttp.model.Uri
2424
import sttp.client3.okhttp.OkHttpSyncBackend
25+
import sttp.model.StatusCode
2526
import za.co.absa.atum.agent.exception.AtumAgentException.HttpException
2627
import za.co.absa.atum.model.ApiPaths
2728
import za.co.absa.atum.model.dto._
@@ -33,7 +34,6 @@ class HttpDispatcher(config: Config) extends Dispatcher(config) with Logging {
3334

3435
private val serverUrl: String = config.getString(UrlKey)
3536

36-
private val apiV1 = s"/${ApiPaths.Api}/${ApiPaths.V1}"
3737
private val apiV2 = s"/${ApiPaths.Api}/${ApiPaths.V2}"
3838

3939
private val getPartitioningIdEndpoint = Uri.unsafeParse(s"$serverUrl$apiV2/${ApiPaths.V2Paths.Partitionings}")
@@ -64,8 +64,8 @@ class HttpDispatcher(config: Config) extends Dispatcher(config) with Logging {
6464

6565
val response = backend.send(request)
6666

67-
response.code.code match {
68-
case 404 => None
67+
response.code match {
68+
case StatusCode.NotFound => None
6969
case _ => Some(handleResponseBody(response).as[SingleSuccessResponse[PartitioningWithIdDTO]].data)
7070
}
7171
}
@@ -104,27 +104,38 @@ class HttpDispatcher(config: Config) extends Dispatcher(config) with Logging {
104104
)
105105
val req = commonAtumRequest.get(endpoint)
106106
val resp = backend.send(req)
107-
handleResponseBody(resp)
108-
.as[SingleSuccessResponse[AdditionalDataDTO.Data]]
109-
.data
110-
.map(item => item._1 -> item._2.map(_.value))
107+
handleResponseBody(resp).as[MultiSuccessResponse[AdditionalDataItemV2DTO]]
111108
}
112109

113110
AtumContextDTO(
114111
partitioning = newPartitioningWithIdDTO.partitioning,
115112
measures = measures,
116-
additionalData = additionalData
113+
additionalData = additionalData.data.map(item => item.key -> item.value).toMap
117114
)
118115
}
119116

120117
override protected[agent] def saveCheckpoint(checkpoint: CheckpointDTO): Unit = {
121-
val endpoint = Uri.unsafeParse(s"$serverUrl$apiV1/${ApiPaths.V1Paths.CreateCheckpoint}")
118+
val partitioningId = getPartitioningId(checkpoint.partitioning)
119+
120+
val checkpointV2DTO = CheckpointV2DTO(
121+
id = checkpoint.id,
122+
name = checkpoint.name,
123+
author = checkpoint.author,
124+
measuredByAtumAgent = checkpoint.measuredByAtumAgent,
125+
processStartTime = checkpoint.processStartTime,
126+
processEndTime = checkpoint.processEndTime,
127+
measurements = checkpoint.measurements,
128+
properties = checkpoint.properties
129+
)
130+
131+
val endpoint = Uri.unsafeParse(
132+
s"$serverUrl$apiV2/${ApiPaths.V2Paths.Partitionings}/$partitioningId/${ApiPaths.V2Paths.Checkpoints}"
133+
)
122134
val request = commonAtumRequest
123135
.post(endpoint)
124-
.body(checkpoint.asJsonString)
136+
.body(checkpointV2DTO.asJsonString)
125137

126138
val response = backend.send(request)
127-
128139
handleResponseBody(response)
129140
}
130141

@@ -145,7 +156,14 @@ class HttpDispatcher(config: Config) extends Dispatcher(config) with Logging {
145156

146157
val response = backend.send(request)
147158

148-
handleResponseBody(response).as[SingleSuccessResponse[AdditionalDataDTO]].data
159+
val data: AdditionalDataDTO.Data = handleResponseBody(response).as[MultiSuccessResponse[AdditionalDataItemV2DTO]]
160+
.data
161+
.map( item => item.value match {
162+
case Some(_) => item.key -> Some(AdditionalDataItemDTO(item.value.get, item.author))
163+
case None => item.key -> None
164+
}).toMap
165+
166+
AdditionalDataDTO(data)
149167
}
150168

151169
private def handleResponseBody(response: Response[Either[String, String]]): String = {

‎agent/src/test/resources/reference.conf‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
atum.dispatcher.type="console"
1616

1717
# The REST API URI of the atum server
18-
# atum.dispatcher.http.url="http://localhost:8080"
18+
#atum.dispatcher.http.url="http://localhost:8080"
1919

2020
# Maximum number of dispatch captures to keep in memory
2121
# atum.dispatcher.capture.capture-limit=1000 # 0 means no limit

‎agent/src/test/scala/za/co/absa/atum/agent/AgentServerCompatibilityTests.scala‎

Lines changed: 39 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -61,28 +61,28 @@ class AgentServerCompatibilityTests extends DBTestSuite {
6161
val rdd = spark.sparkContext.parallelize(testDataForRDD)
6262
val df = spark.createDataFrame(rdd, testDataSchema)
6363

64-
// Atum Agent preparation - Agent configured to work with HTTP Dispatcher and service on localhost
65-
val agent: AtumAgent = new AtumAgent {
66-
override val dispatcher: HttpDispatcher = new HttpDispatcher(
67-
ConfigFactory
68-
.empty()
69-
.withValue("atum.dispatcher.type", ConfigValueFactory.fromAnyRef("http"))
70-
.withValue("atum.dispatcher.http.url", ConfigValueFactory.fromAnyRef("http://localhost:8080"))
71-
)
72-
}
73-
74-
// Atum Context stuff preparation - Partitioning, Measures, Additional Data, Checkpoint
75-
val domainAtumPartitioning = ListMap(
76-
"partition1" -> "valueFromTest1",
77-
"partition2" -> "valueFromTest2"
78-
)
79-
val domainAtumContext = agent.getOrCreateAtumContext(domainAtumPartitioning)
80-
81-
domainAtumContext.addMeasure(RecordCount())
82-
domainAtumContext.addAdditionalData("author", "Laco")
83-
domainAtumContext.addAdditionalData(Map("author" -> "LacoNew", "version" -> "1.0"))
84-
85-
domainAtumContext.createCheckpoint("checkPointNameCount", df)
64+
// Atum Agent preparation - Agent configured to work with HTTP Dispatcher and service on localhost
65+
val agent: AtumAgent = new AtumAgent {
66+
override val dispatcher: HttpDispatcher = new HttpDispatcher(
67+
ConfigFactory
68+
.empty()
69+
.withValue("atum.dispatcher.type", ConfigValueFactory.fromAnyRef("http"))
70+
.withValue("atum.dispatcher.http.url", ConfigValueFactory.fromAnyRef("http://localhost:8080"))
71+
)
72+
}
73+
74+
// Atum Context stuff preparation - Partitioning, Measures, Additional Data, Checkpoint
75+
val domainAtumPartitioning = ListMap(
76+
"partition1" -> "valueFromTest1",
77+
"partition2" -> "valueFromTest2"
78+
)
79+
val domainAtumContext = agent.getOrCreateAtumContext(domainAtumPartitioning)
80+
81+
domainAtumContext.addMeasure(RecordCount())
82+
domainAtumContext.addAdditionalData("author", "Laco")
83+
domainAtumContext.addAdditionalData(Map("author" -> "LacoNew", "version" -> "1.0"))
84+
85+
domainAtumContext.createCheckpoint("checkPointNameCount", df)
8686

8787
// DB Check, data should be written in the DB
8888
table("runs.partitionings").all() { partitioningsResult =>
@@ -94,35 +94,45 @@ class AgentServerCompatibilityTests extends DBTestSuite {
9494
}
9595

9696
table("runs.additional_data").all() { adResult =>
97+
val expectedMap = Map(
98+
"author" -> "LacoNew",
99+
"version" -> "1.0"
100+
)
101+
97102
assert(adResult.hasNext)
98103
val row = adResult.next()
99104

100-
assert(row.getString("ad_name").contains("author"))
101-
assert(row.getString("ad_value").contains("LacoNew"))
105+
val adName1 = row.getString("ad_name").get
106+
val adValue1 = row.getString("ad_value").get
107+
108+
assert(expectedMap(adName1) == adValue1)
102109

103110
assert(adResult.hasNext)
104111
val row2 = adResult.next()
105112

106-
assert(row2.getString("ad_name").contains("version"))
107-
assert(row2.getString("ad_value").contains("1.0"))
113+
val adName2 = row2.getString("ad_name").get
114+
val adValue2 = row2.getString("ad_value").get
115+
116+
assert(expectedMap(adName2) == adValue2)
117+
108118
assert(!adResult.hasNext)
109119
}
110120

111-
table("runs.additional_data_history").all() { adHistResult =>
121+
table("runs.additional_data_history").all() { adHistResult =>
112122
assert(adHistResult.hasNext)
113123
val row = adHistResult.next()
114124

115125
assert(row.getString("ad_name").contains("author"))
116126
assert(row.getString("ad_value").contains("Laco"))
117127

118-
assert(!adHistResult.hasNext)
128+
assert(!adHistResult.hasNext) // failing when executed multiple times
119129
}
120130

121131
table("runs.measure_definitions").all() { measureDefResult =>
122132
assert(measureDefResult.hasNext)
123133
val row = measureDefResult.next()
124134

125-
assert(row.getString("measure_name").contains("*"))
135+
assert(row.getString("measure_name").contains("count"))
126136
assert(row.getString("measured_columns").contains("{}"))
127137

128138
assert(!measureDefResult.hasNext)
@@ -135,7 +145,5 @@ class AgentServerCompatibilityTests extends DBTestSuite {
135145
assert(row.getJsonB("measurement_value").contains(expectedMeasurement))
136146
assert(!measurementsResult.hasNext)
137147
}
138-
139-
// TODO Truncate data potentially, Balta might support this in the near future
140148
}
141149
}

‎agent/src/test/scala/za/co/absa/atum/agent/dispatcher/HttpDispatcherUnitTests.scala‎

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,20 @@ package za.co.absa.atum.agent.dispatcher
22

33
import org.scalatest.flatspec.AnyFlatSpec
44
import org.scalatest.matchers.should.Matchers
5-
import org.scalatest.BeforeAndAfterEach
5+
import org.scalatest.{BeforeAndAfterEach, Ignore}
66
import org.mockito.Mockito._
77
import org.mockito.ArgumentMatchers._
88
import sttp.client3._
99
import sttp.model.StatusCode
1010
import com.typesafe.config.Config
1111
import sttp.capabilities
12+
import za.co.absa.atum.agent.exception.AtumAgentException.HttpException
1213
import za.co.absa.atum.model.dto._
1314
import za.co.absa.atum.model.envelopes.SuccessResponse.{MultiSuccessResponse, SingleSuccessResponse}
1415
import za.co.absa.atum.model.utils.JsonSyntaxExtensions.JsonSerializationSyntax
1516

17+
import java.time.ZonedDateTime
18+
1619
class HttpDispatcherUnitTests extends AnyFlatSpec with Matchers with BeforeAndAfterEach {
1720

1821
var mockBackend: SttpBackend[Identity, sttp.capabilities.WebSockets] = _
@@ -23,9 +26,9 @@ class HttpDispatcherUnitTests extends AnyFlatSpec with Matchers with BeforeAndAf
2326
val testPartitioningSubmitDTO = PartitioningSubmitDTO(testPartitioningDTO, None, "author")
2427
val createdPartitioningWithId = PartitioningWithIdDTO(123L, testPartitioningDTO, "author")
2528
val measures: Seq[MeasureDTO] = Seq(MeasureDTO("m1", Seq("c1")), MeasureDTO("m2", Seq("c2")))
26-
val additionalData: Map[String, Option[AdditionalDataItemDTO]] = Map(
27-
"key1" -> Some(AdditionalDataItemDTO("val1", "author1")),
28-
"key2" -> None
29+
val additionalData: Seq[AdditionalDataItemV2DTO] = Seq(
30+
AdditionalDataItemV2DTO("key1", Some("val1"), "author1"),
31+
AdditionalDataItemV2DTO("key2", None, "author2")
2932
)
3033

3134
def encodedPartitioning(partitioning: Seq[PartitionDTO]): String =
@@ -100,7 +103,7 @@ class HttpDispatcherUnitTests extends AnyFlatSpec with Matchers with BeforeAndAf
100103
val measuresResponse =
101104
Response(Right(MultiSuccessResponse(measures).asJsonString): Either[String, String], StatusCode.Ok)
102105
val additionalDataResponse =
103-
Response(Right(SingleSuccessResponse(additionalData).asJsonString): Either[String, String], StatusCode.Ok)
106+
Response(Right(MultiSuccessResponse[AdditionalDataItemV2DTO](additionalData).asJsonString): Either[String, String], StatusCode.Ok)
104107

105108
stubGetPartitioning(parentPartitioning, getParentResponse)
106109
stubGetPartitioning(testPartitioningDTO, getPartitioningResponse)
@@ -158,7 +161,7 @@ class HttpDispatcherUnitTests extends AnyFlatSpec with Matchers with BeforeAndAf
158161
val measuresResponse =
159162
Response(Right(MultiSuccessResponse(Seq.empty[MeasureDTO]).asJsonString): Either[String, String], StatusCode.Ok)
160163
val additionalDataResponse = Response(
161-
Right(SingleSuccessResponse(Map.empty[String, Option[AdditionalDataItemDTO]]).asJsonString): Either[
164+
Right(MultiSuccessResponse(Seq.empty[AdditionalDataItemV2DTO]).asJsonString): Either[
162165
String,
163166
String
164167
],
@@ -177,4 +180,83 @@ class HttpDispatcherUnitTests extends AnyFlatSpec with Matchers with BeforeAndAf
177180
result.measures shouldBe empty
178181
result.additionalData shouldBe empty
179182
}
183+
184+
"saveCheckpoint" should "successfully save a checkpoint when partitioning exists" in {
185+
val checkpoint = CheckpointDTO(
186+
id = java.util.UUID.randomUUID(),
187+
name = "cp",
188+
author = "author",
189+
measuredByAtumAgent = true,
190+
processStartTime = ZonedDateTime.now(),
191+
processEndTime = Some(ZonedDateTime.now()),
192+
measurements = Set.empty,
193+
partitioning = testPartitioningDTO
194+
)
195+
val partitioningWithId = PartitioningWithIdDTO(123L, testPartitioningDTO, "author")
196+
val getPartitioningResponse = Response(
197+
Right(SingleSuccessResponse(partitioningWithId).asJsonString): Either[String, String],
198+
StatusCode.Ok
199+
)
200+
val postCheckpointResponse = Response(Right("{}"): Either[String, String], StatusCode.Created)
201+
202+
stubGetPartitioning(testPartitioningDTO, getPartitioningResponse)
203+
when(
204+
mockBackend.send(
205+
argThat[Request[Either[String, String], capabilities.WebSockets]](
206+
req => req.method.method == "POST" && req.uri.path.mkString.contains("checkpoints")
207+
)
208+
)
209+
).thenReturn(postCheckpointResponse)
210+
211+
noException should be thrownBy dispatcher.saveCheckpoint(checkpoint)
212+
}
213+
214+
it should "throw HttpException if partitioning is not found" in {
215+
val checkpoint = CheckpointDTO(
216+
id = java.util.UUID.randomUUID(),
217+
name = "cp",
218+
author = "author",
219+
measuredByAtumAgent = true,
220+
processStartTime = ZonedDateTime.now(),
221+
processEndTime = Some(ZonedDateTime.now()),
222+
measurements = Set.empty,
223+
partitioning = testPartitioningDTO
224+
)
225+
val notFoundResponse = Response(Left("Not found"): Either[String, String], StatusCode.NotFound)
226+
227+
stubGetPartitioning(testPartitioningDTO, notFoundResponse)
228+
229+
an[HttpException] should be thrownBy dispatcher.saveCheckpoint(checkpoint)
230+
}
231+
232+
it should "throw HttpException if server returns error on checkpoint save" in {
233+
val checkpoint = CheckpointDTO(
234+
id = java.util.UUID.randomUUID(),
235+
name = "cp",
236+
author = "author",
237+
measuredByAtumAgent = true,
238+
processStartTime = ZonedDateTime.now(),
239+
processEndTime = Some(ZonedDateTime.now()),
240+
measurements = Set.empty,
241+
partitioning = testPartitioningDTO
242+
)
243+
val partitioningWithId = PartitioningWithIdDTO(123L, testPartitioningDTO, "author")
244+
val getPartitioningResponse = Response(
245+
Right(SingleSuccessResponse(partitioningWithId).asJsonString): Either[String, String],
246+
StatusCode.Ok
247+
)
248+
val badRequestResponse = Response(Left("Bad request"): Either[String, String], StatusCode.BadRequest)
249+
250+
stubGetPartitioning(testPartitioningDTO, getPartitioningResponse)
251+
when(
252+
mockBackend.send(
253+
argThat[Request[Either[String, String], capabilities.WebSockets]](
254+
req => req.method.method == "POST" && req.uri.path.mkString.contains("checkpoints")
255+
)
256+
)
257+
).thenReturn(badRequestResponse)
258+
259+
an[HttpException] should be thrownBy dispatcher.saveCheckpoint(checkpoint)
260+
}
261+
180262
}

0 commit comments

Comments
 (0)