Skip to content

Commit 2b94758

Browse files
committed
merge feature/progress-and-retry and resolve merge conflict
Merge branch 'feature/progress-and-retry' into v0.5.11 # Conflicts: # R/cancensus.R # man/get_census.Rd
2 parents b10ebba + 68f56f1 commit 2b94758

5 files changed

Lines changed: 154 additions & 11 deletions

File tree

R/cancensus.R

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#' for each census vector variable, containing the original suppression codes (e.g., 'x', 'F', '...')
2525
#' before they are converted to NA. Useful for understanding data quality and suppression patterns.
2626
#' Default is FALSE. Use \code{\link{census_data_quality}} to analyze suppression patterns.
27+
#' @param retry Integer If greater than zero, automatically retry failed API requests with exponential backoff for specified maximum number of times. Defaults to 0.
2728
#'
2829
#' @source Census data and boundary geographies are reproduced and distributed on
2930
#' an "as is" basis with the permission of Statistics Canada (Statistics Canada 1996; 2001; 2006; 2011; 2016).
@@ -56,8 +57,8 @@ get_census <- function (dataset, regions, level=NA, vectors=c(), geo_format = NA
5657
resolution = 'simplified',
5758
labels = "detailed",
5859
use_cache=TRUE, quiet=FALSE, api_key=Sys.getenv("CM_API_KEY"),
59-
preserve_suppression_flags=FALSE)
60-
{
60+
preserve_suppression_flags=FALSE,
61+
retry=0) {
6162

6263
# API and data recall checks
6364
first_run_checks()
@@ -149,13 +150,32 @@ get_census <- function (dataset, regions, level=NA, vectors=c(), geo_format = NA
149150
stop(paste("No API key set. Use set_cancensus_api_key('<your API key>') to set one, or set_cancensus_api_key('<your API key>', install = TRUE) to save it permanently in your .Renviron."))
150151
}
151152
url <- paste0(base_url, "data.csv")
152-
response <- if (!quiet) {
153-
message("Querying CensusMapper API...")
154-
httr::POST(url, httr::progress(), body=params)
153+
if (!quiet) message("Querying CensusMapper API for data...")
154+
155+
# Define the API call
156+
make_data_call <- function() {
157+
if (!quiet) {
158+
httr::POST(url, httr::progress(), body=params)
159+
} else {
160+
httr::POST(url, body=params)
161+
}
162+
}
163+
164+
# Execute with or without retry
165+
response <- if (retry>0) {
166+
retry_api_call(make_data_call, max_retries = retry, quiet = quiet)
155167
} else {
156-
httr::POST(url, body=params)
168+
make_data_call()
157169
}
170+
158171
handle_cm_status_code(response, NULL)
172+
173+
# Report download size
174+
content_length <- as.numeric(response$headers$`content-length`)
175+
if (!quiet && !is.na(content_length) && content_length > 0) {
176+
message(sprintf("Downloaded %s of data.", format_bytes(content_length)))
177+
}
178+
159179
data_version <- response$headers$`data-version`
160180

161181

@@ -221,13 +241,32 @@ get_census <- function (dataset, regions, level=NA, vectors=c(), geo_format = NA
221241
stop(paste("No API key set. Use set_cancensus_api_key('<your API key>') to set one, or set_cancensus_api_key('<your API key>', install = TRUE) to save it permanently in your .Renviron."))
222242
}
223243
url <- paste0(base_url, "geo.geojson")
224-
response <- if (!quiet) {
225-
message("Querying CensusMapper API...")
226-
httr::POST(url, httr::progress(),body=params)
244+
if (!quiet) message("Querying CensusMapper API for geometry...")
245+
246+
# Define the API call
247+
make_geo_call <- function() {
248+
if (!quiet) {
249+
httr::POST(url, httr::progress(), body=params)
250+
} else {
251+
httr::POST(url, body=params)
252+
}
253+
}
254+
255+
# Execute with or without retry
256+
response <- if (retry) {
257+
retry_api_call(make_geo_call, max_retries = max_retries, quiet = quiet)
227258
} else {
228-
httr::POST(url,body=params)
259+
make_geo_call()
229260
}
261+
230262
handle_cm_status_code(response, NULL)
263+
264+
# Report download size
265+
content_length <- as.numeric(response$headers$`content-length`)
266+
if (!quiet && !is.na(content_length) && content_length > 0) {
267+
message(sprintf("Downloaded %s of geometry.", format_bytes(content_length)))
268+
}
269+
231270
geo_version <- response$headers$`data-version`
232271
write(httr::content(response, type = "text", encoding = "UTF-8"), file = geo_file) # cache result
233272
file_info <- file.info(geo_file)
@@ -245,6 +284,9 @@ get_census <- function (dataset, regions, level=NA, vectors=c(), geo_format = NA
245284
transform_geo(level) #%>%
246285
# sf::st_sf(agr="constant") # just in case
247286

287+
if (!quiet) {
288+
message(sprintf("Processing %s geographic features.", format(nrow(geos), big.mark = ",")))
289+
}
248290

249291
result <- if (is.null(result)) {
250292
geos

R/helpers.R

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,70 @@ dataset_from_vector_list <- function(vector_list){
8282

8383
cancensus_na_strings <- c("x", "X", "F", "...", "..", "-","N","*","**")
8484

85+
#' Retry an API call with exponential backoff
86+
#'
87+
#' @param call_fn A function that performs the API call and returns an httr response
88+
#' @param max_retries Maximum number of retry attempts (default: 3)
89+
#' @param quiet If TRUE, suppress retry messages
90+
#' @return The httr response object
91+
#' @keywords internal
92+
retry_api_call <- function(call_fn, max_retries = 3, quiet = FALSE) {
93+
attempt <- 1
94+
last_error <- NULL
95+
96+
while (attempt <= max_retries) {
97+
tryCatch({
98+
response <- call_fn()
99+
100+
# Check for transient HTTP errors (5xx, timeout, connection errors)
101+
status <- httr::status_code(response)
102+
if (status >= 500 && status < 600 && attempt < max_retries) {
103+
# Server error - retry
104+
wait_time <- 2 ^ (attempt - 1) # Exponential backoff: 1, 2, 4 seconds
105+
if (!quiet) {
106+
message(sprintf("Server error (HTTP %d), retrying in %ds (attempt %d/%d)...",
107+
status, wait_time, attempt + 1, max_retries))
108+
}
109+
Sys.sleep(wait_time)
110+
attempt <- attempt + 1
111+
next
112+
}
113+
114+
# Success or non-retryable error
115+
return(response)
116+
117+
}, error = function(e) {
118+
last_error <<- e
119+
# Network errors - retry
120+
if (attempt < max_retries) {
121+
wait_time <- 2 ^ (attempt - 1)
122+
if (!quiet) {
123+
message(sprintf("Network error: %s. Retrying in %ds (attempt %d/%d)...",
124+
conditionMessage(e), wait_time, attempt + 1, max_retries))
125+
}
126+
Sys.sleep(wait_time)
127+
attempt <<- attempt + 1
128+
} else {
129+
stop(e)
130+
}
131+
})
132+
}
133+
134+
# If we've exhausted retries, throw the last error
135+
if (!is.null(last_error)) {
136+
stop(last_error)
137+
}
138+
}
139+
140+
#' Format bytes to human-readable size
141+
#' @keywords internal
142+
format_bytes <- function(bytes) {
143+
if (bytes < 1024) return(paste0(bytes, " B"))
144+
if (bytes < 1024^2) return(sprintf("%.1f KB", bytes / 1024))
145+
if (bytes < 1024^3) return(sprintf("%.1f MB", bytes / 1024^2))
146+
return(sprintf("%.1f GB", bytes / 1024^3))
147+
}
148+
85149
as.num = function(x, na.strings = cancensus_na_strings) {
86150
stopifnot(is.character(x))
87151
na = x %in% na.strings

man/format_bytes.Rd

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/get_census.Rd

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/retry_api_call.Rd

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)