Skip to content

Create a new piezo endpoint for updating data - #136

Open
jdforde wants to merge 1 commit into
mainfrom
SRE-14470-create-piezo-update-data-endpoint
Open

jdforde wants to merge 1 commit into
mainfrom
SRE-14470-create-piezo-update-data-endpoint

Conversation

@jdforde

@jdforde jdforde commented Sep 3, 2026

Copy link
Copy Markdown

Create a new piezo endpoint that allows us to update job data for multiple jobs at once

@jdforde
jdforde requested a review from tmccombs September 3, 2026 17:47

@ifndefJOSH ifndefJOSH left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just have a couple of questions about whether or not there's any potentially sensitive information about jobs in error messages that could leak out?

val errorMsg = "Exception caught updating the job data map of job %s %s. -- %s".format(
jobKey.getGroup,
jobKey.getName,
e.getLocalizedMessage(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this exception passed through to the HTTP response? If so we need to verify nothing sensitive is exposed.

"updated" -> updated.map(jobKeyJson),
"failures" -> failures,
)
if (failures.isEmpty) Ok(body) else MultiStatus(body)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious who will be calling this endpoint? Other services or will it be public? If the latter, failures need to be sanitized to ensure no sensitive data leaks out.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intention is another service. but piezo doesn't really have a way to distinguish

Comment thread admin/conf/routes
GET /data/jobs com.lucidchart.piezo.admin.controllers.Jobs.getJobsDetail
GET /data/jobs/:group/:name com.lucidchart.piezo.admin.controllers.Jobs.getJobDetail(group: String, name: String)

POST /data/jobs/job-data-map com.lucidchart.piezo.admin.controllers.Jobs.patchJobDataMaps

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious why this is a POST vs a PATCH request which the controller method implies.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it matters too much in this case.

@tate8

tate8 commented Sep 3, 2026

Copy link
Copy Markdown

Could an in progress job persist its job data after this endpoint updates the lastRunTime thereby overwriting the reset? Does there need to be some mechanism to pause new instances of these jobs from starting and waiting for running executions? This is a rare race condition though but just double checking.

* The body looks like
* {{{
* {
* "jobs": [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we anticipate having additional fields here? or would it make sense to just accept the array directly without the "jobs" field?

* Errors are collected across the whole `jobs` list, so a caller patching hundreds of jobs sees every bad entry at
* once rather than one per round trip.
*/
def parse(json: JsValue): JsResult[List[JobDataMapPatch]] = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: It might be a little cleaner if this was something like

private implicit val reads: Reads[JobDataMapPatch] = Reads(parseJob) // or inline parseJob

private val listReads = (JsPath \ "jobs").read[List[JobDataMapPatch]]

def parse(json: JsValue): JsResult[List[JobDataMapPatch]] = json.validate(listReads)


private def parseJob(json: JsValue): JsResult[JobDataMapPatch] = {
for {
group <- pathed((json \ "group").validate[String], JsPath \ "group")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of using this pathed function you can get equivalent functionality with

group <- json.validate((JsPath \ "group").read[String])

or

group <- (JsPath \ "group").read[String].reads(json)

} yield JobDataMapPatch(new JobKey(name, group), entries)
}

private def parseEntries(dataMap: JsLookupResult): JsResult[List[DataMap]] = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why accept an array or an object? this could be simpler if we only accept one, and we don't have to worry about backwards compatibility?

}
}

private def parsePairEntry(pair: JsValue): JsResult[DataMap] = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to keep this, we should make it a reads on thedefinition of Datamap

}

def patchJobDataMaps: Action[JsValue] = Action(parse.json(maxFormSize)) { request =>
JobDataMapPatch.parse(request.body) match {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we define a Reads for the jobdatamappatch, we can have play handle the parsing (and returning errors for invalid data) for us by using parse.json[List[JobDataMapPatch]] (if we just take an array) or parse.json(JobDataMapPatch.listReads) if we want to keep the wrapper object.

"updated" -> updated.map(jobKeyJson),
"failures" -> failures,
)
if (failures.isEmpty) Ok(body) else MultiStatus(body)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intention is another service. but piezo doesn't really have a way to distinguish

case JsSuccess(patches, _) =>
val (failures, updated) = patches.map(applyJobDataMapPatch).partitionMap(identity)
val body = Json.obj(
"count" -> patches.size,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems unnecessary, shouldn't the caller already know what the count is?

Comment thread admin/conf/routes
GET /data/jobs com.lucidchart.piezo.admin.controllers.Jobs.getJobsDetail
GET /data/jobs/:group/:name com.lucidchart.piezo.admin.controllers.Jobs.getJobDetail(group: String, name: String)

POST /data/jobs/job-data-map com.lucidchart.piezo.admin.controllers.Jobs.patchJobDataMaps

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it matters too much in this case.

import scala.concurrent.Future
import scala.jdk.CollectionConverters.*

class JobDataMapService extends Specification {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this called a service?

This looks like a test, but it doesn't have Test or Spec in the name

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants