Skip to content

Commit 5233573

Browse files
authored
Enable Soft Deletion Backfills (#443)
Allow soft-delete backfills for cancelled and completed backfills. Soft Deletion Handling: Backfila is updated to recognize and process records flagged as soft-deleted, ensuring they are included or excluded from backfills based on configuration.​ A 'Delete' button has been added to cancelled or completed backfills in the UI, providing users with the ability to soft delete backfills. We filter the deleted backfills from the UI. https://github.com/user-attachments/assets/2f755a1f-b0c7-4b95-a515-8b6d000dc320 Modified to nullable timestamp ![Screenshot 2025-04-29 at 1 01 39 PM](https://github.com/user-attachments/assets/7cbd8040-9633-4fa3-baf4-caa9ae227e78)
1 parent ddd4562 commit 5233573

File tree

8 files changed

+117
-0
lines changed

8 files changed

+117
-0
lines changed

service/src/main/kotlin/app/cash/backfila/dashboard/GetBackfillRunsAction.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ class GetBackfillRunsAction @Inject constructor(
102102
val (pausedBackfills, nextOffset) = queryFactory.newQuery<BackfillRunQuery>()
103103
.serviceId(dbService.id)
104104
.stateNot(BackfillState.RUNNING)
105+
.notSoftDeleted()
105106
.filterByArgs(filterArgs)
106107
.newPager(
107108
idDescPaginator(),

service/src/main/kotlin/app/cash/backfila/dashboard/GetBackfillStatusAction.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ data class GetBackfillStatusResponse(
6565
val backoff_schedule: String?,
6666
val partitions: List<UiPartition>,
6767
val event_logs: List<UiEventLog>,
68+
val deleted_at: Instant?,
6869
)
6970

7071
class GetBackfillStatusAction @Inject constructor(
@@ -98,6 +99,7 @@ class GetBackfillStatusAction @Inject constructor(
9899
run.backoff_schedule,
99100
partitions.map { dbToUi(it) },
100101
events(session, run, partitions),
102+
run.deleted_at,
101103
)
102104
}
103105
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package app.cash.backfila.dashboard
2+
3+
import app.cash.backfila.service.persistence.BackfilaDb
4+
import app.cash.backfila.service.persistence.BackfillState
5+
import app.cash.backfila.service.persistence.DbBackfillRun
6+
import app.cash.backfila.service.persistence.DbEventLog
7+
import javax.inject.Inject
8+
import misk.MiskCaller
9+
import misk.exceptions.BadRequestException
10+
import misk.hibernate.Id
11+
import misk.hibernate.Transacter
12+
import misk.hibernate.load
13+
import misk.scope.ActionScoped
14+
import misk.security.authz.Authenticated
15+
import misk.web.PathParam
16+
import misk.web.Post
17+
import misk.web.RequestContentType
18+
import misk.web.ResponseContentType
19+
import misk.web.actions.WebAction
20+
import misk.web.mediatype.MediaTypes
21+
22+
class SoftDeleteBackfillAction @Inject constructor(
23+
@BackfilaDb private val transacter: Transacter,
24+
private val caller: @JvmSuppressWildcards ActionScoped<MiskCaller?>,
25+
) : WebAction {
26+
@Post("/backfill/delete/{id}")
27+
@RequestContentType(MediaTypes.APPLICATION_JSON)
28+
@ResponseContentType(MediaTypes.APPLICATION_JSON)
29+
@Authenticated(capabilities = ["users"])
30+
fun softDelete(
31+
@PathParam id: Long,
32+
) {
33+
transacter.transaction { session ->
34+
val backfillRun = session.load<DbBackfillRun>(Id(id))
35+
36+
// Only allow soft delete for COMPLETE or CANCELLED backfills
37+
if (backfillRun.state != BackfillState.COMPLETE && backfillRun.state != BackfillState.CANCELLED) {
38+
throw BadRequestException("Can only delete completed or cancelled backfills")
39+
}
40+
41+
backfillRun.deleted_at = java.time.Instant.now()
42+
43+
// Log the deletion event
44+
session.save(
45+
DbEventLog(
46+
backfillRun.id,
47+
partition_id = null,
48+
user = caller.get()?.principal ?: "",
49+
type = DbEventLog.Type.STATE_CHANGE,
50+
message = "backfill soft deleted",
51+
),
52+
)
53+
}
54+
}
55+
}

service/src/main/kotlin/app/cash/backfila/service/persistence/BackfillRunQuery.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,7 @@ interface BackfillRunQuery : Query<DbBackfillRun> {
3030

3131
@Order("updated_at", asc = false)
3232
fun orderByUpdatedAtDesc(): BackfillRunQuery
33+
34+
@Constraint("deleted_at", Operator.IS_NULL)
35+
fun notSoftDeleted(): BackfillRunQuery
3336
}

service/src/main/kotlin/app/cash/backfila/service/persistence/DbBackfillRun.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ class DbBackfillRun() : DbUnsharded<DbBackfillRun>, DbTimestampedEntity {
9090
@Column(nullable = false)
9191
var dry_run: Boolean = false
9292

93+
@Column(nullable = true)
94+
var deleted_at: Instant? = null
95+
9396
/** Comma separated list of delays for consecutive retries in milliseconds, e.g. 1000,2000 */
9497
@Column
9598
var backoff_schedule: String? = null

service/src/main/kotlin/app/cash/backfila/ui/actions/BackfillShowButtonHandlerAction.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package app.cash.backfila.ui.actions
22

33
import app.cash.backfila.dashboard.CancelBackfillAction
4+
import app.cash.backfila.dashboard.SoftDeleteBackfillAction
45
import app.cash.backfila.dashboard.StartBackfillAction
56
import app.cash.backfila.dashboard.StartBackfillRequest
67
import app.cash.backfila.dashboard.StopBackfillAction
@@ -33,6 +34,7 @@ class BackfillShowButtonHandlerAction @Inject constructor(
3334
private val stopBackfillAction: StopBackfillAction,
3435
private val updateBackfillAction: UpdateBackfillAction,
3536
private val cancelBackfillAction: CancelBackfillAction,
37+
private val softDeleteBackfillAction: SoftDeleteBackfillAction,
3638
) : WebAction {
3739
@Get(PATH)
3840
@ResponseContentType(MediaTypes.TEXT_HTML)
@@ -56,6 +58,9 @@ class BackfillShowButtonHandlerAction @Inject constructor(
5658
BackfillState.CANCELLED.name -> {
5759
cancelBackfillAction.cancel(id.toLong())
5860
}
61+
"soft_delete" -> {
62+
softDeleteBackfillAction.softDelete(id.toLong())
63+
}
5964
}
6065
}
6166

service/src/main/kotlin/app/cash/backfila/ui/pages/BackfillShowAction.kt

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import app.cash.backfila.ui.components.DashboardPageLayout
1010
import app.cash.backfila.ui.components.PageTitle
1111
import app.cash.backfila.ui.components.ProgressBar
1212
import app.cash.backfila.ui.pages.BackfillCreateAction.BackfillCreateField.CUSTOM_PARAMETER_PREFIX
13+
import java.time.Instant
1314
import javax.inject.Inject
1415
import javax.inject.Singleton
1516
import kotlinx.html.ButtonType
@@ -260,6 +261,7 @@ class BackfillShowAction @Inject constructor(
260261
val button: Link? = null,
261262
val updateFieldId: String? = null,
262263
val cancelButton: Link? = null,
264+
val deleteButton: Link? = null,
263265
)
264266

265267
private fun getStateButton(state: BackfillState): Link? {
@@ -288,13 +290,27 @@ class BackfillShowAction @Inject constructor(
288290
}
289291
}
290292

293+
private fun getDeleteButton(state: BackfillState, deletedAt: Instant?): Link? {
294+
if (deletedAt != null) {
295+
return null
296+
}
297+
return when (state) {
298+
BackfillState.COMPLETE, BackfillState.CANCELLED -> Link(
299+
label = DELETE_STATE_BUTTON_LABEL,
300+
href = "soft_delete",
301+
)
302+
else -> null
303+
}
304+
}
305+
291306
private fun GetBackfillStatusResponse.toConfigurationRows(id: Long) = listOf(
292307
DescriptionListRow(
293308
label = "State",
294309
description = state.name,
295310
button = getStateButton(state),
296311
updateFieldId = "state",
297312
cancelButton = getCancelButton(state),
313+
deleteButton = getDeleteButton(state, deleted_at),
298314
),
299315
DescriptionListRow(
300316
label = "Dry Run",
@@ -533,6 +549,35 @@ class BackfillShowAction @Inject constructor(
533549
}
534550
}
535551
}
552+
553+
it.deleteButton?.let { deleteButton ->
554+
span("ml-2") {
555+
form {
556+
action = BackfillShowButtonHandlerAction.path(id)
557+
558+
it.updateFieldId?.let {
559+
input {
560+
type = InputType.hidden
561+
name = "field_id"
562+
value = it
563+
}
564+
565+
input {
566+
type = InputType.hidden
567+
name = "field_value"
568+
value = deleteButton.href
569+
}
570+
}
571+
572+
button(
573+
classes = "rounded-full bg-gray-600 px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-gray-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-600",
574+
) {
575+
type = ButtonType.submit
576+
+deleteButton.label
577+
}
578+
}
579+
}
580+
}
536581
}
537582
}
538583
}
@@ -582,6 +627,7 @@ class BackfillShowAction @Inject constructor(
582627
const val START_STATE_BUTTON_LABEL = "Start"
583628
const val PAUSE_STATE_BUTTON_LABEL = "Pause"
584629
const val CANCEL_STATE_BUTTON_LABEL = "Cancel"
630+
const val DELETE_STATE_BUTTON_LABEL = "Delete"
585631
const val UPDATE_BUTTON_LABEL = "Update"
586632
const val VIEW_LOGS_BUTTON_LABEL = "View Logs"
587633
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ALTER TABLE backfill_runs
2+
ADD COLUMN `deleted_at` timestamp(3) NULL DEFAULT NULL;

0 commit comments

Comments
 (0)