Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v1 Scheduled Jobs / Third Party Sync Migration #171

Open
wants to merge 48 commits into
base: master
Choose a base branch
from

Conversation

mattwilshire
Copy link
Member

  • Adds the ability to create scheduled jobs (currently only for third party syncs) that run off of a cron schedule.
  • Third party sync configurations are pulled from the application.properties and pushed to a new scheduled_job table.
  • The ScheduledJobManager handles the creation and deletion of jobs.
  • The job listener updates the status of each job pre and post execution.
  • New endpoints for fetching, triggering and toggling scheduled jobs.

JobKey jobKey = scheduledJobManager.getJobKey(scheduledJob);

try {
if (!scheduledJobManager.getScheduler().checkExists(jobKey))
Copy link
Collaborator

Choose a reason for hiding this comment

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

You get the scheduler 3 distinct times in this method, would it not be possible to store it in a variable and just reuse the result? Or does the scheduler vary a lot and you need to wait to the last possible minute to get it each time?

Copy link
Member Author

Choose a reason for hiding this comment

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

It would require capturing the scheduler in a static variable if it hasn't been set yet as this a web service, I believe this way is fine and I haven't seen other web services cache objects.

}
}

@RequestMapping(method = RequestMethod.POST, value = "/api/jobs/{id}/toggle")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: toggle is a strange term for this. I get that you mean it disable and enable a job, but this name feels confusing

Copy link
Collaborator

Choose a reason for hiding this comment

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

Honestly I have a preference for separate endpoints for enable and disable because this current endpoint expects the called to know what the previous state was. This could make debugging very difficult.

Example: I called toggled on a job, but I used the wrong job's id. Now the output could either be that it is toggle or not toggled depending on its prior state, which we don't know since this was a mistake. It adds a layer of "what was it before? because that dictates what it is now". Where as the if the endpoint is /enable you know if the status code is 200, then it is enabled now. No confusion, no mental gymnastics required

Copy link
Member Author

Choose a reason for hiding this comment

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

Sounds good, I'll change it to two separate endpoints!

Copy link
Collaborator

Choose a reason for hiding this comment

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

+1 to having idempotent APIs, i.e.: separate enable/disable here

Copy link
Member Author

Choose a reason for hiding this comment

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

Done - @byronantak

@Autowired ServerConfig serverConfig;

@Value(
"${l10n.scheduledJobs.thirdPartySync.notifications.title:MOJITO | Third party sync failed for {repository}}")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Sorry, I am a bit of a loss here. I know that the ${} syntax loops in the application properties to get the value but how does the {repository} bit work? That's not received from app properties and there doesn't appear to be a variable in scope that it can read from...

Copy link
Member Author

@mattwilshire mattwilshire Oct 25, 2024

Choose a reason for hiding this comment

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

If you use ${} in the application properties it will evaluate it on startup, if it doesn't find the variable, the process will crash straight away (IIRC). Other approach is to use {0}, {1} but that relies on the message having those parameters which isn't flexible

See:
String title = StrSubstitutor.replace( notificationTitle, ImmutableMap.of("repository", scheduledJob.getRepository().getName()), "{", "}");

If {repository} is in the custom title it will be replaced with the repository name that did the third party sync.

"${l10n.scheduledJobs.thirdPartySync.notifications.title:MOJITO | Third party sync failed for {repository}}")
String notificationTitle;

private ScheduledJob scheduledJob;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Also a possible misunderstanding but why do we need to store these variables as class level variables? Does the context not have the details to find them again? 🤔

I don't know this library but I'm mainly worried about side-effects.

Copy link
Member Author

Choose a reason for hiding this comment

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

The notification title is pulled in from the application properties, you can have different PagerDuty titles for different environments. The ScheduledJob here is set on execution, we pull it out of the database on the execute method, when the success or failure method is called by the listener we can reference the job as its tied to the instance.

@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
// Fetch the scheduled job and cast the properties
scheduledJob = scheduledJobRepository.findByJobKey(jobExecutionContext.getJobDetail().getKey());
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should this not be the try as well?
What if the cast fails or if the key is not found (by some misfortune)

Copy link
Member Author

Choose a reason for hiding this comment

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

The only way for this not to exist if there was some manual altering of the database, the manager creates a job for reach scheduled job so it must exist otherwise it would never even get to this execute method.

try {
pd.triggerIncident(scheduledJob.getId(), payload);
} catch (PagerDutyException e) {
logger.error(
Copy link
Collaborator

Choose a reason for hiding this comment

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

You take a lot of time and effort to build up all those useful urls above and if the incident fails to trigger, then you just discard it?
Can we not log it too to save us the trouble of reverse engineering the links? (It might be included in the payload, I'm not sure. Just the thought which was triggered)

Copy link
Member Author

Choose a reason for hiding this comment

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

@mattwilshire mattwilshire force-pushed the mattwilshire/v1-thirdparty-sync-migration branch from 0b9f4fa to a9a1336 Compare October 31, 2024 12:18
@Override
public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) {
// If the job is disabled, don't execute
ScheduledJob job = scheduledJobRepository.findByJobKey(trigger.getJobKey());
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we perhaps handle the case where the job might not be found?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Unlikely, I know but not impossible

Copy link
Member Author

Choose a reason for hiding this comment

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

Out of date comment @byronantak

package com.box.l10n.mojito.service.scheduledjob;

public abstract class ScheduledJobProperties {
private int version = 1;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm wondering if we should use semantic versioning here 🤔 Probably overkill

Copy link
Member Author

@mattwilshire mattwilshire Nov 4, 2024

Choose a reason for hiding this comment

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

No, this is only for casting to newer versions, it doesn't require semantic versioning.

@Column(name = "name")
@Enumerated(EnumType.STRING)
@JsonView(View.Repository.class)
private com.box.l10n.mojito.service.scheduledjob.ScheduledJobStatus jobStatus;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we use an import here? This looks funky to me

Copy link
Member Author

@mattwilshire mattwilshire Nov 4, 2024

Choose a reason for hiding this comment

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

No we can't, the name of the imported class matches the name of the current class, the definition needs to be this specific.

@Column(name = "name")
@Enumerated(EnumType.STRING)
@JsonView(View.Repository.class)
private com.box.l10n.mojito.service.scheduledjob.ScheduledJobType jobType;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Similar comment as before

Copy link
Member Author

@mattwilshire mattwilshire Nov 4, 2024

Choose a reason for hiding this comment

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

Check previous comment.

public class ScheduledJobType extends BaseEntity {
@Id private Long id;

@Basic(optional = false)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Hold on. Why is this needed? The column is not not nullable, so should the config not reflect that. This Basic property to my understanding enforces that the property is not null locally but not at a db level (I might be wrong)

Copy link
Member Author

@mattwilshire mattwilshire Nov 4, 2024

Choose a reason for hiding this comment

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

I have updated the other columns that are not nullable to have this, I noticed it was on other entity objects so I decided to use it. It looks like you can generate SQL schemas off of entity mappings, not that we would need to use this but best practice to have annotations like this!

@mattwilshire mattwilshire marked this pull request as ready for review November 4, 2024 18:21
@mattwilshire mattwilshire requested a review from a team as a code owner November 4, 2024 18:21
new ResponseStatusException(HttpStatus.NOT_FOUND, "Job not found with id: " + id));
}

@RequestMapping(method = RequestMethod.POST, value = "/api/jobs/{id}")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just looking at a POST to "/api/jobs/{id}" suggests to me that we might be updating some information for that job or submitting a new/updated job definition (as the get at on the same path/route returns us back the information about the job).
(We might also want to reserve the POST/PUT to that route for dynamically creating job definitions in the future)

Thinking of using something more explicit here, similar to enable/disable, e.g.:
"/api/jobs/{id}/trigger" ?

@Audited
@Entity
@Table(name = "scheduled_job")
public class ScheduledJob {
Copy link
Member

Choose a reason for hiding this comment

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

Should we extend BaseEntity here, which will then provide the id field handling in the parent class? Might have missed a previous conversation about this

JobKey jobKey = scheduledJobManager.getJobKey(scheduledJob);

try {
if (!scheduledJobManager.getScheduler().checkExists(jobKey)) return notFoundResponse;
Copy link
Member

Choose a reason for hiding this comment

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

Can getScheduler() return null in any case? If so do we need handling for that here?

this.endDate = endDate;
}

public Boolean getEnabled() {
Copy link
Member

Choose a reason for hiding this comment

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

Should this be named isEnabled()?

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