Skip to content

Commit bb734f9

Browse files
ivicacclaude
andcommitted
4756 Fix principal_job FK violation when deleting project deployment
Split the delete loop into two passes — delete all principal_job rows first, then delete all jobs. The previous single-pass loop deleted parent's principal_job and then called jobFacade.deleteJob(parent), which recursively deletes child jobs whose principal_job rows still existed, causing fk_principal_job_job violations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 858dbbe commit bb734f9

5 files changed

Lines changed: 156 additions & 5 deletions

File tree

server/ee/libs/embedded/embedded-configuration/embedded-configuration-service/src/main/java/com/bytechef/ee/embedded/configuration/facade/IntegrationInstanceConfigurationFacadeImpl.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,9 @@ public void deleteIntegrationInstanceConfiguration(long id) {
237237
triggerExecutionService.deleteJobTriggerExecution(jobId);
238238

239239
principalJobService.deletePrincipalJobs(jobId, PlatformType.EMBEDDED);
240+
}
240241

242+
for (long jobId : jobIds) {
241243
jobFacade.deleteJob(jobId);
242244
}
243245

server/libs/atlas/atlas-execution/atlas-execution-service/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@ dependencies {
1111
implementation(project(":server:libs:core:commons:commons-util"))
1212

1313
testImplementation("org.springframework.data:spring-data-jdbc")
14+
testImplementation("tools.jackson.core:jackson-databind")
15+
testImplementation(project(":server:libs:atlas:atlas-configuration:atlas-configuration-converter"))
1416
testImplementation(project(":server:libs:atlas:atlas-configuration:atlas-configuration-repository:atlas-configuration-repository-jdbc"))
1517
testImplementation(project(":server:libs:atlas:atlas-execution:atlas-execution-repository:atlas-execution-repository-jdbc"))
1618
testImplementation(project(":server:libs:config:liquibase-config"))
19+
testImplementation(project(":server:libs:core:commons:commons-data"))
1720
testImplementation(project(":server:libs:test:test-int-support"))
1821
}

server/libs/atlas/atlas-execution/atlas-execution-service/src/test/java/com/bytechef/atlas/execution/facade/JobFacadeIntTest.java

Lines changed: 120 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,41 @@
1616

1717
package com.bytechef.atlas.execution.facade;
1818

19+
import static org.assertj.core.api.Assertions.assertThat;
20+
21+
import com.bytechef.atlas.configuration.converter.StringToWorkflowTaskConverter;
22+
import com.bytechef.atlas.configuration.converter.WorkflowTaskToStringConverter;
23+
import com.bytechef.atlas.configuration.domain.WorkflowTask;
1924
import com.bytechef.atlas.configuration.service.WorkflowService;
25+
import com.bytechef.atlas.execution.domain.Job;
26+
import com.bytechef.atlas.execution.domain.TaskExecution;
2027
import com.bytechef.atlas.execution.dto.JobParametersDTO;
2128
import com.bytechef.atlas.execution.repository.JobRepository;
29+
import com.bytechef.atlas.execution.repository.TaskExecutionRepository;
30+
import com.bytechef.atlas.execution.repository.jdbc.converter.StringToWebhooksConverter;
31+
import com.bytechef.atlas.execution.repository.jdbc.converter.WebhooksToStringConverter;
2232
import com.bytechef.atlas.execution.service.ContextService;
2333
import com.bytechef.atlas.execution.service.JobService;
2434
import com.bytechef.atlas.execution.service.JobServiceImpl;
2535
import com.bytechef.atlas.execution.service.TaskExecutionService;
36+
import com.bytechef.atlas.execution.service.TaskExecutionServiceImpl;
2637
import com.bytechef.atlas.file.storage.TaskFileStorage;
38+
import com.bytechef.commons.data.jdbc.converter.ExecutionErrorToStringConverter;
39+
import com.bytechef.commons.data.jdbc.converter.FileEntryToStringConverter;
40+
import com.bytechef.commons.data.jdbc.converter.MapWrapperToStringConverter;
41+
import com.bytechef.commons.data.jdbc.converter.StringToFileEntryConverter;
42+
import com.bytechef.commons.data.jdbc.converter.StringToMapWrapperConverter;
2743
import com.bytechef.jackson.config.JacksonConfiguration;
2844
import com.bytechef.liquibase.config.LiquibaseConfiguration;
2945
import com.bytechef.test.config.jdbc.AbstractIntTestJdbcConfiguration;
3046
import com.bytechef.test.config.testcontainers.PostgreSQLContainerConfiguration;
47+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
48+
import java.util.Arrays;
3149
import java.util.Collections;
50+
import java.util.HashMap;
51+
import java.util.List;
52+
import java.util.Map;
53+
import org.apache.commons.lang3.Validate;
3254
import org.junit.jupiter.api.Assertions;
3355
import org.junit.jupiter.api.Test;
3456
import org.springframework.beans.factory.annotation.Autowired;
@@ -41,6 +63,7 @@
4163
import org.springframework.context.annotation.Import;
4264
import org.springframework.data.jdbc.repository.config.EnableJdbcAuditing;
4365
import org.springframework.test.context.bean.override.mockito.MockitoBean;
66+
import tools.jackson.databind.ObjectMapper;
4467

4568
/**
4669
* Ivica Cardic
@@ -57,14 +80,80 @@ public class JobFacadeIntTest {
5780
@Autowired
5881
private JobFacade jobFacade;
5982

83+
@Autowired
84+
private JobRepository jobRepository;
85+
86+
@Autowired
87+
private TaskExecutionRepository taskExecutionRepository;
88+
6089
@Test
6190
public void testRequiredParameters() {
6291
Assertions.assertThrows(
6392
NullPointerException.class,
6493
() -> jobFacade.createJob(new JobParametersDTO("aGVsbG8x", Collections.emptyMap())));
6594
}
6695

67-
@ComponentScan(basePackages = "com.bytechef.atlas.execution.facade")
96+
@Test
97+
public void testDeleteJobRecursivelyDeletesChildJobsAndTaskExecutions() {
98+
Job parentJob = jobRepository.save(newJob());
99+
long parentJobId = Validate.notNull(parentJob.getId(), "id");
100+
101+
TaskExecution parentTaskExecution = taskExecutionRepository.save(newTaskExecution(parentJobId, null));
102+
103+
long parentTaskExecutionId = Validate.notNull(parentTaskExecution.getId(), "id");
104+
105+
Job childJob = jobRepository.save(newJob());
106+
107+
childJob.setParentTaskExecutionId(parentTaskExecutionId);
108+
109+
jobRepository.save(childJob);
110+
111+
long childJobId = Validate.notNull(childJob.getId(), "id");
112+
113+
TaskExecution childTaskExecution = taskExecutionRepository.save(newTaskExecution(childJobId, null));
114+
115+
long childTaskExecutionId = Validate.notNull(childTaskExecution.getId(), "id");
116+
117+
jobFacade.deleteJob(parentJobId);
118+
119+
assertThat(jobRepository.findById(parentJobId)).isEmpty();
120+
assertThat(jobRepository.findById(childJobId)).isEmpty();
121+
assertThat(taskExecutionRepository.findById(parentTaskExecutionId)).isEmpty();
122+
assertThat(taskExecutionRepository.findById(childTaskExecutionId)).isEmpty();
123+
}
124+
125+
private static Job newJob() {
126+
Job job = new Job();
127+
128+
job.setStatus(Job.Status.COMPLETED);
129+
job.setWorkflowId("demo:1234");
130+
131+
return job;
132+
}
133+
134+
private static TaskExecution newTaskExecution(long jobId, Long parentId) {
135+
Map<String, Object> taskMap = new HashMap<>();
136+
137+
taskMap.put("name", "task1");
138+
taskMap.put("type", "test/v1/noop");
139+
140+
TaskExecution taskExecution = TaskExecution.builder()
141+
.workflowTask(new WorkflowTask(taskMap))
142+
.build();
143+
144+
taskExecution.setJobId(jobId);
145+
taskExecution.setTaskNumber(1);
146+
147+
if (parentId != null) {
148+
taskExecution.setParentId(parentId);
149+
}
150+
151+
return taskExecution;
152+
}
153+
154+
@ComponentScan(basePackages = {
155+
"com.bytechef.atlas.execution.facade", "com.bytechef.atlas.configuration.converter"
156+
})
68157
@EnableAutoConfiguration
69158
@Configuration
70159
public static class WorkflowExecutionIntTestConfiguration {
@@ -75,14 +164,14 @@ public static class WorkflowExecutionIntTestConfiguration {
75164
@MockitoBean
76165
private WorkflowService workflowService;
77166

78-
@MockitoBean
79-
private TaskExecutionService taskExecutionService;
80-
81167
@MockitoBean
82168
private TaskFileStorage taskFileStorage;
83169

84170
@Bean
85-
JobFacade jobFacade(ApplicationEventPublisher eventPublisher, JobService jobService) {
171+
JobFacade jobFacade(
172+
ApplicationEventPublisher eventPublisher, JobService jobService,
173+
TaskExecutionService taskExecutionService) {
174+
86175
return new JobFacadeImpl(
87176
eventPublisher, contextService, jobService, taskExecutionService, taskFileStorage, workflowService);
88177
}
@@ -92,8 +181,34 @@ JobService jobService(JobRepository jobRepository) {
92181
return new JobServiceImpl(jobRepository);
93182
}
94183

184+
@Bean
185+
TaskExecutionService taskExecutionService(TaskExecutionRepository taskExecutionRepository) {
186+
return new TaskExecutionServiceImpl(taskExecutionRepository);
187+
}
188+
95189
@EnableJdbcAuditing(auditorAwareRef = "auditorProvider", dateTimeProviderRef = "auditingDateTimeProvider")
96190
public static class WorkflowIntTestJdbcConfiguration extends AbstractIntTestJdbcConfiguration {
191+
192+
private final ObjectMapper objectMapper;
193+
194+
@SuppressFBWarnings("EI")
195+
public WorkflowIntTestJdbcConfiguration(ObjectMapper objectMapper) {
196+
this.objectMapper = objectMapper;
197+
}
198+
199+
@Override
200+
protected List<?> userConverters() {
201+
return Arrays.asList(
202+
new ExecutionErrorToStringConverter(objectMapper),
203+
new FileEntryToStringConverter(objectMapper),
204+
new MapWrapperToStringConverter(objectMapper),
205+
new StringToFileEntryConverter(objectMapper),
206+
new StringToMapWrapperConverter(objectMapper),
207+
new StringToWebhooksConverter(objectMapper),
208+
new StringToWorkflowTaskConverter(objectMapper),
209+
new WebhooksToStringConverter(objectMapper),
210+
new WorkflowTaskToStringConverter(objectMapper));
211+
}
97212
}
98213
}
99214
}

server/libs/automation/automation-configuration/automation-configuration-service/src/main/java/com/bytechef/automation/configuration/facade/ProjectDeploymentFacadeImpl.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,9 @@ public void deleteProjectDeployment(long id) {
222222
triggerExecutionService.deleteJobTriggerExecution(jobId);
223223

224224
principalJobService.deletePrincipalJobs(jobId, PlatformType.AUTOMATION);
225+
}
225226

227+
for (long jobId : jobIds) {
226228
jobFacade.deleteJob(jobId);
227229
}
228230

server/libs/automation/automation-configuration/automation-configuration-service/src/test/java/com/bytechef/automation/configuration/facade/ProjectDeploymentFacadeIntTest.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import static org.mockito.ArgumentMatchers.anyString;
2525
import static org.mockito.ArgumentMatchers.eq;
2626
import static org.mockito.Mockito.atLeastOnce;
27+
import static org.mockito.Mockito.inOrder;
2728
import static org.mockito.Mockito.reset;
2829
import static org.mockito.Mockito.verify;
2930
import static org.mockito.Mockito.when;
@@ -60,6 +61,7 @@
6061
import org.junit.jupiter.api.BeforeEach;
6162
import org.junit.jupiter.api.Disabled;
6263
import org.junit.jupiter.api.Test;
64+
import org.mockito.InOrder;
6365
import org.springframework.beans.factory.annotation.Autowired;
6466
import org.springframework.boot.test.context.SpringBootTest;
6567
import org.springframework.context.annotation.Import;
@@ -188,6 +190,33 @@ public void testDeleteProjectDeployment() {
188190
assertThat(workspaceProjectDeployments).hasSize(0);
189191
}
190192

193+
@Test
194+
public void testDeleteProjectDeploymentRemovesAllPrincipalJobsBeforeAnyJob() {
195+
ProjectDTO projectDTO = projectDeploymentFacadeHelper.createProject(workspace.getId());
196+
ProjectDeploymentDTO projectDeploymentDTO =
197+
projectDeploymentFacadeHelper.createProjectDeployment(workspace.getId(), projectDTO);
198+
199+
long deploymentId = projectDeploymentDTO.id();
200+
long parentJobId = 501L;
201+
long childJobId = 502L;
202+
203+
when(principalJobService.getJobIds(deploymentId, PlatformType.AUTOMATION))
204+
.thenReturn(List.of(parentJobId, childJobId));
205+
206+
projectDeploymentFacade.deleteProjectDeployment(deploymentId);
207+
208+
InOrder inOrder = inOrder(principalJobService, jobFacade);
209+
210+
inOrder.verify(principalJobService)
211+
.deletePrincipalJobs(parentJobId, PlatformType.AUTOMATION);
212+
inOrder.verify(principalJobService)
213+
.deletePrincipalJobs(childJobId, PlatformType.AUTOMATION);
214+
inOrder.verify(jobFacade)
215+
.deleteJob(parentJobId);
216+
inOrder.verify(jobFacade)
217+
.deleteJob(childJobId);
218+
}
219+
191220
@Disabled
192221
@Test
193222
public void testGetProjectDeployment() {

0 commit comments

Comments
 (0)