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

Add dispatchable workflow for deleting test model runs #60

Merged
merged 24 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a35baca
Add delete-model-runs workflow
jeancochrane Nov 16, 2023
c5c2404
Temporarily run a readonly version of the delete-model-runs workflow…
jeancochrane Nov 16, 2023
3f0b8c1
Install libgit2-dev for git2r in delete-model-runs.yaml
jeancochrane Nov 16, 2023
d70deea
Fix R styler error in delete_current_year_model_runs.R
jeancochrane Nov 16, 2023
26c61bb
Clean up docstrings and test against 2023-11-14-frosty-jacob model
jeancochrane Nov 16, 2023
3297673
Satisfy pre-commit
jeancochrane Nov 16, 2023
51843f3
Revert to workflow_dispatch event trigger for delete-model-runs.yaml
jeancochrane Nov 16, 2023
49ea1a6
Revert "Revert to workflow_dispatch event trigger for delete-model-ru…
jeancochrane Nov 16, 2023
85bfcf8
Fix typo in Delete model runs step of delete-model-runs workflow
jeancochrane Nov 16, 2023
61b9352
Test obviously bogus value for run-ids to delete-model-runs workflow
jeancochrane Nov 16, 2023
a3a28fa
Raise an error in delete_current_year_model_runs.R if no objects were…
jeancochrane Nov 16, 2023
5c9ddf6
Disable renv sandbox to speed up install/run times
jeancochrane Nov 17, 2023
a090cc1
Run delete_current_year_model_runs.R on all run IDs at once
jeancochrane Nov 17, 2023
c624b0b
Check for validity of run IDs before issuing delete operations in del…
jeancochrane Nov 17, 2023
17fcf90
Refactor run ID validity check in delete_current_year_model_runs to b…
jeancochrane Nov 17, 2023
b6e695d
Try deleting multiple invalid run IDs
jeancochrane Nov 17, 2023
0e56190
Refactor delete_current_year_model_runs.R to accept a comma-delimited…
jeancochrane Nov 17, 2023
cf4ff3c
Test a delete-model-runs workflow where one run is valid and one isn't
jeancochrane Nov 17, 2023
e23d34c
Test a delete-model-runs workflow where all run IDs are valid
jeancochrane Nov 17, 2023
6cdb449
Revert to workflow_dispatch trigger for delete-model-runs.yaml
jeancochrane Nov 17, 2023
b02de7d
Remove extraneous print statement from delete_current_year_model_runs.R
jeancochrane Nov 17, 2023
a4895c2
Clean up delete-model-runs and associated script in response to review
jeancochrane Nov 20, 2023
09cfa5b
Temporarily run delete-model-runs on pull_request event for testing
jeancochrane Nov 20, 2023
759550d
Revert "Temporarily run delete-model-runs on pull_request event for t…
jeancochrane Nov 20, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .github/workflows/delete-model-runs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Workflow that can be manually dispatched to delete test model runs that
# do not need to be persisted indefinitely.
#
# Gated such that it's impossible to delete runs older than the current
# assessment cycle, where each assessment cycle starts in April.

name: delete-model-runs

on:
workflow_dispatch:
inputs:
run-ids:
description: >
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: IMO it'd be really helpful here to have a default input showing an example (nonexistent) run ID.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

True, done in a4895c2! Note that the trade-off here is that default arguments are populated as actual values in the workflow input form, rather than placeholder text, so there's a chance of a caller misinterpreting the form and submitting it with the (fake) default values. This should just raise an error and not actually delete any data, however, so the risk is worth it in my view.

Comma-delimited list of IDs of model runs to delete (no spaces). Note
that the workflow assumes these IDs correspond to model runs for the
current upcoming assessment cycle, and if that's not the case the
deletion script will raise an error.
required: true
type: string

jobs:
delete-model-runs:
runs-on: ubuntu-latest
permissions:
# Needed to interact with GitHub's OIDC Token endpoint so we can auth AWS
contents: read
id-token: write
steps:
- name: Checkout repo code
uses: actions/checkout@v4

- name: Setup R
uses: r-lib/actions/setup-r@v2
with:
use-public-rspm: true

- name: Install system dependencies
run: sudo apt-get install libgit2-dev
shell: bash
jeancochrane marked this conversation as resolved.
Show resolved Hide resolved

- name: Disable renv sandbox to speed up install time
run: echo "RENV_CONFIG_SANDBOX_ENABLED=FALSE" >> .Renviron
shell: bash
jeancochrane marked this conversation as resolved.
Show resolved Hide resolved

- name: Setup renv
uses: r-lib/actions/setup-renv@v2
jeancochrane marked this conversation as resolved.
Show resolved Hide resolved

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_IAM_ROLE_MODEL_DELETION_ARN }}
aws-region: us-east-1

- name: Delete model runs
run: Rscript ./R/delete_current_year_model_runs.R "$RUN_IDS"
shell: bash
env:
RUN_IDS: ${{ inputs.run-ids }}
Copy link
Member

Choose a reason for hiding this comment

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

nitpick: Can't you just pass the input directly here?

Suggested change
run: Rscript ./R/delete_current_year_model_runs.R "$RUN_IDS"
shell: bash
env:
RUN_IDS: ${{ inputs.run-ids }}
run: Rscript ./R/delete_current_year_model_runs.R ${{ inputs.run-ids }}
shell: bash

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This would be indeed be cleaner! I decided to switch the format of the run-ids input variable to expect a space-separated string in a4895c2, however, so I made a slightly different change here, instead using the RUN_IDS env var to strip spaces and replace them with commas. (My thinking is that spaces provide a clearer interface for the GitHub workflow, but on the off chance we ever need to run R/delete_current_year_model_runs.R manually, a single space-delimited string would be a much more confusing command line interface than a single comma-delimited string. Accepting a space-delimited string in the GitHub UI but a comma-delimited string via the command line seems like a reasonable compromise to me.)

83 changes: 83 additions & 0 deletions R/delete_current_year_model_runs.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Script to delete a list of model runs by ID from AWS.
#
# Accepts one argument, a comma-delimited list of run IDs for model runs
# whose artifacts should be deleted.
#
# Assumes that model runs are restricted to the current assessment cycle, where
# each assessment cycle starts in April. Raises an error if no objects matching
# a given ID for the current year could be located in S3. This error will get
# raised before any deletion occurs, so if one or more IDs are invalid then
# no objects will be deleted.
#
# Example usage (delete the runs 123, 456, and 789 in the current year):
#
# delete_current_year_model_runs.R 123,456,789

library(glue)
library(here)
library(magrittr)
source(here("R", "helpers.R"))

current_date <- as.POSIXct(Sys.Date())
current_month <- current_date %>% format("%m")
current_year <- current_date %>% format("%Y")

# The following heuristic determines the current upcoming assessment cycle year:
#
# * From April to December (post assessment), `year` = next year
# * From January to March (during assessment), `year` = current year
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Does this heuristic seem reasonable to you?

Copy link
Member

Choose a reason for hiding this comment

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

Totally! I would bump the cutoff month to May though, as sometimes we still do model touchup stuff in April.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in a4895c2!

year <- if (current_month < "03") {
current_year
} else {
as.character(as.numeric(current_year) + 1)
}

# Convert the comma-delimited input to a vector of run IDs. Accepting one or
# more positional arguments would be a cleaner UX, but since this script is
# intended to be called from a dispatched GitHub workflow, it's easier to parse
# one comma-delimited string than split a space-separated string passed as a
# workflow input
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agonized over this interface, perhaps unnecessarily; I'm curious if you agree that a single comma-delimited string is the right compromise!

Copy link
Member

Choose a reason for hiding this comment

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

I think it's fine! Though we might want to use a space-delimited string in order to match build-and-test-dbt. It would have to be quoted i.e. not positional args. I'm really fine with either though as long as it's clear what the input is supposed to be.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Cool, I switched the workflow input to accept a space-delimited string in a4895c2.

raw_run_ids <- commandArgs(trailingOnly = TRUE)
run_ids <- raw_run_ids %>%
strsplit(split = ",", fixed = TRUE) %>%
unlist()

"Confirming artifacts exist for run IDs in year {year}: {raw_run_ids}" %>%
glue::glue() %>%
print()

# We consider a run ID to be valid if it has any matching data in S3 for
# the current year
run_id_is_valid <- function(run_id, year) {
return(
model_get_s3_artifacts_for_run(run_id, year) %>%
sapply(aws.s3::object_exists) %>%
any()
)
}

# We check for validity separate from the deletion operation for two reasons:
#
# 1. The aws.s3::delete_object API does not raise an error if an object does
# not exist, so a delete operation alone won't alert us for an incorrect
# ID
# 2. Even if aws.s3::delete_object could raise an error for missing objects,
# we want to alert the caller that one or more of the IDs were incorrect
# before deleting any objects so that this script is nondestructive
# in the case of a malformed ID
valid_run_ids <- run_ids %>% sapply(run_id_is_valid, year = year)

if (!all(valid_run_ids)) {
invalid_run_ids <- run_ids[which(valid_run_ids == FALSE)] %>%
paste(collapse = ", ")

"Some run IDs are missing all S3 artifacts for {year}: {invalid_run_ids}" %>%
glue::glue() %>%
stop()
jeancochrane marked this conversation as resolved.
Show resolved Hide resolved
}

"Deleting S3 artifacts run IDs in year {year}: {run_ids}" %>%
glue::glue() %>%
print()
Comment on lines +79 to +81
Copy link
Contributor Author

Choose a reason for hiding this comment

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

To test deletion, I copied over run artifacts for two different model runs from year=2023/ to year=2024/ and deleted the 2024 versions. Example workflow here: https://github.com/ccao-data/model-res-avm/actions/runs/6908697011/job/18798517688

In order to delete existing test model runs from year=2023/, my thinking was that we could make a list of model runs we want to delete, and then I can move them from year=2023/ to year=2024/ and delete them. I'll need your signoff on this list to make sure I don't delete anything important, and I'll plan to do this task after merging this PR.

Copy link
Member

Choose a reason for hiding this comment

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

That works for me! Let's also delete other test runs created during the model infra construction.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good, will do!


run_ids %>% sapply(model_delete_run, year = year)
31 changes: 17 additions & 14 deletions R/helpers.R
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,16 @@ model_file_dict <- function(run_id = NULL, year = NULL) {
return(dict)
}


# Used to delete erroneous, incomplete, or otherwise unwanted runs
# Use with caution! Deleted models are retained for a period of time before
# being permanently deleted
model_delete_run <- function(run_id, year) {
# Get a vector of S3 paths to the artifacts for a given model run
model_get_s3_artifacts_for_run <- function(run_id, year) {
jeancochrane marked this conversation as resolved.
Show resolved Hide resolved
# Get paths of all run objects based on the file dictionary
paths <- model_file_dict(run_id, year)
s3_objs <- grep("s3://", unlist(paths), value = TRUE)
bucket <- strsplit(s3_objs[1], "/")[[1]][3]

# First get anything partitioned only by year
s3_objs_limited <- grep(".parquet$|.zip$|.rds$", s3_objs, value = TRUE)
s3_objs_limited <- grep(".parquet$|.zip$|.rds$", s3_objs, value = TRUE) %>%
unname()

# Next get the prefix of anything partitioned by year and run_id
s3_objs_dir_path <- file.path(
Expand All @@ -53,16 +51,21 @@ model_delete_run <- function(run_id, year) {
)
s3_objs_dir_path <- gsub(paste0("s3://", bucket, "/"), "", s3_objs_dir_path)
s3_objs_dir_path <- gsub("//", "/", s3_objs_dir_path)
s3_objs_w_run_id <- unlist(purrr::map(
s3_objs_dir_path,
~ aws.s3::get_bucket_df(bucket, .x)$Key
))

# Delete current version of objects
purrr::walk(s3_objs_limited, aws.s3::delete_object)
purrr::walk(s3_objs_w_run_id, aws.s3::delete_object, bucket = bucket)
s3_objs_w_run_id <- s3_objs_dir_path %>%
purrr::map(~ aws.s3::get_bucket_df(bucket, .x)$Key) %>%
unlist() %>%
purrr::map_chr(~ glue::glue("s3://{bucket}/{.x}"))

return(c(s3_objs_limited, s3_objs_w_run_id))
jeancochrane marked this conversation as resolved.
Show resolved Hide resolved
}

# Used to delete erroneous, incomplete, or otherwise unwanted runs
# Use with caution! Deleted models are retained for a period of time before
# being permanently deleted
model_delete_run <- function(run_id, year) {
model_get_s3_artifacts_for_run(run_id, year) %>%
purrr::walk(aws.s3::delete_object)
}

# Used to fetch a run's output from S3 and populate it locally. Useful for
# running reports and performing local troubleshooting
Expand Down
Loading