Skip to content

Commit b791dec

Browse files
committed
v2.0.5 wip
1 parent 2caa053 commit b791dec

6 files changed

Lines changed: 158 additions & 2 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ jobs:
8383
query_file_path: ci-scripts/get-contributors.iql
8484
query_output: csv
8585
- name: Save contributors CSV
86-
run: echo "${{ steps.get-contributors.outputs.stackql-query-results }}" > contributors.csv
86+
run: echo "${{ steps.get-contributors.outputs.stackql-query-results }}" | tail -n +2 > contributors.csv
8787
- name: Upload contributors artifact
8888
uses: actions/upload-artifact@v7
8989
with:

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ jobs:
4646
query_file_path: ci-scripts/get-contributors.iql
4747
query_output: csv
4848
- name: Save contributors CSV
49-
run: echo "${{ steps.get-contributors.outputs.stackql-query-results }}" > contributors.csv
49+
run: echo "${{ steps.get-contributors.outputs.stackql-query-results }}" | tail -n +2 > contributors.csv
5050
- name: Upload contributors artifact
5151
uses: actions/upload-artifact@v7
5252
with:

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ stackql*.pkg
77
stackql_history.txt
88
stackql.log
99
stackql-zip
10+
stackql-deploy
1011
.env
1112
nohup.out
1213
contributors.csv

src/core/errors.rs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
//! Fatal error detection for StackQL query execution.
2+
//!
3+
//! Maintains a list of error patterns that indicate unrecoverable failures
4+
//! (network issues, auth failures, etc.) vs normal operational errors
5+
//! (404 not found) that the retry/statecheck logic can handle.
6+
7+
/// Error patterns that indicate a fatal, non-retryable failure.
8+
///
9+
/// These are checked against the error message string returned by the
10+
/// StackQL engine. If any pattern matches, the operation is aborted
11+
/// immediately rather than retried.
12+
///
13+
/// Two categories:
14+
///
15+
/// 1. **Network errors** - The request never reached the API. Any result
16+
/// from a query in this state is untrustworthy (e.g., an exists check
17+
/// returning empty could cause a duplicate resource to be created).
18+
///
19+
/// 2. **HTTP status errors** - The request reached the API but the response
20+
/// indicates an unrecoverable problem (auth failure, forbidden, etc.).
21+
/// 404 is explicitly excluded as it's normal for exists checks.
22+
const FATAL_ERROR_PATTERNS: &[&str] = &[
23+
// Network-layer errors (Go net/http)
24+
"dial tcp:",
25+
"Client.Timeout exceeded",
26+
"connection refused",
27+
"no such host",
28+
"request canceled while waiting for connection",
29+
"request canceled (Client.Timeout",
30+
"tls: handshake",
31+
"certificate",
32+
"network is unreachable",
33+
"connection reset by peer",
34+
"broken pipe",
35+
"EOF",
36+
// HTTP status codes that are never retryable
37+
"http response status code: 401",
38+
"http response status code: 403",
39+
];
40+
41+
/// Patterns that indicate a non-fatal error, even if a fatal pattern
42+
/// also matches. These take precedence over `FATAL_ERROR_PATTERNS`.
43+
///
44+
/// For example, a 404 is normal for exists checks on resources that
45+
/// don't exist yet.
46+
const NON_FATAL_OVERRIDES: &[&str] = &[
47+
"http response status code: 404",
48+
"ResourceNotFoundException",
49+
"was not found",
50+
];
51+
52+
/// Check if an error message indicates a fatal, non-retryable failure.
53+
///
54+
/// Returns `Some(reason)` if the error is fatal, `None` if it's
55+
/// a normal operational error that can be retried or handled.
56+
pub fn check_fatal_error(error_msg: &str) -> Option<&'static str> {
57+
// First check if any non-fatal override matches
58+
for pattern in NON_FATAL_OVERRIDES {
59+
if error_msg.contains(pattern) {
60+
return None;
61+
}
62+
}
63+
64+
// Then check for fatal patterns
65+
for pattern in FATAL_ERROR_PATTERNS {
66+
if error_msg.contains(pattern) {
67+
return Some(pattern);
68+
}
69+
}
70+
71+
None
72+
}
73+
74+
#[cfg(test)]
75+
mod tests {
76+
use super::*;
77+
78+
#[test]
79+
fn test_network_timeout_is_fatal() {
80+
let msg = r#"Query execution failed: query returns error: Post "https://cloudcontrolapi.us-east-1.amazonaws.com/?Action=GetResource&Version=2021-09-30": net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)"#;
81+
assert!(check_fatal_error(msg).is_some());
82+
}
83+
84+
#[test]
85+
fn test_dns_failure_is_fatal() {
86+
let msg = r#"Query execution failed: query returns error: Post "https://cloudcontrolapi.us-east-1.amazonaws.com/": dial tcp: lookup cloudcontrolapi.us-east-1.amazonaws.com on 8.8.4.4:53: i/o timeout"#;
87+
assert!(check_fatal_error(msg).is_some());
88+
}
89+
90+
#[test]
91+
fn test_403_is_fatal() {
92+
let msg = r#"http response status code: 403, response body: {"message":"Access Denied"}"#;
93+
assert!(check_fatal_error(msg).is_some());
94+
}
95+
96+
#[test]
97+
fn test_401_is_fatal() {
98+
let msg = r#"http response status code: 401, response body: {"message":"Unauthorized"}"#;
99+
assert!(check_fatal_error(msg).is_some());
100+
}
101+
102+
#[test]
103+
fn test_404_is_not_fatal() {
104+
let msg = r#"http response status code: 404, response body: {"__type":"ResourceNotFoundException","Message":"Resource not found"}"#;
105+
assert!(check_fatal_error(msg).is_none());
106+
}
107+
108+
#[test]
109+
fn test_resource_not_found_is_not_fatal() {
110+
let msg = r#"Resource of type 'AWS::EC2::VPC' with identifier 'vpc-xxx' was not found"#;
111+
assert!(check_fatal_error(msg).is_none());
112+
}
113+
114+
#[test]
115+
fn test_400_bad_request_is_not_fatal() {
116+
let msg = r#"insert over HTTP error: 400 Bad Request"#;
117+
assert!(check_fatal_error(msg).is_none());
118+
}
119+
120+
#[test]
121+
fn test_normal_query_error_is_not_fatal() {
122+
let msg = r#"query returns error: no such column: foo"#;
123+
assert!(check_fatal_error(msg).is_none());
124+
}
125+
}

src/core/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@
77
88
pub mod config;
99
pub mod env;
10+
pub mod errors;
1011
pub mod templating;
1112
pub mod utils;

src/core/utils.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use std::time::{Duration, Instant};
1313

1414
use log::{debug, error, info};
1515

16+
use crate::core::errors::check_fatal_error;
1617
use crate::utils::pgwire::PgwireLite;
1718
use crate::utils::query::{execute_query, QueryResult};
1819

@@ -95,6 +96,13 @@ pub fn run_stackql_query(
9596
if !result_maps.is_empty() {
9697
if let Some(err) = result_maps[0].get("error") {
9798
last_error = Some(err.clone());
99+
// Check for fatal errors even when suppressing
100+
if let Some(pattern) = check_fatal_error(err) {
101+
catch_error_and_exit(&format!(
102+
"Fatal error (matched '{}'):\n\n{}\n",
103+
pattern, err
104+
));
105+
}
98106
if !suppress_errors {
99107
if attempt == retries {
100108
catch_error_and_exit(&format!(
@@ -155,6 +163,13 @@ pub fn run_stackql_query(
155163
Err(e) => {
156164
last_error = Some(e.clone());
157165
debug!("Query error on attempt {}: {}", attempt + 1, e);
166+
// Check for fatal errors (network, auth) that should not be retried
167+
if let Some(pattern) = check_fatal_error(&e) {
168+
catch_error_and_exit(&format!(
169+
"Fatal error (matched '{}'):\n\n{}\n",
170+
pattern, e
171+
));
172+
}
158173
if attempt == retries && !suppress_errors {
159174
catch_error_and_exit(&format!(
160175
"Exception during stackql query execution:\n\n{}\n",
@@ -249,6 +264,13 @@ pub fn run_stackql_command(
249264
}
250265
}
251266
Err(e) => {
267+
// Check for fatal errors (network, auth) before retrying
268+
if let Some(pattern) = check_fatal_error(&e) {
269+
catch_error_and_exit(&format!(
270+
"Fatal error (matched '{}'):\n\n{}\n",
271+
pattern, e
272+
));
273+
}
252274
if !ignore_errors {
253275
if attempt < retries {
254276
debug!(
@@ -759,6 +781,13 @@ pub fn run_stackql_dml_returning(
759781
}
760782
},
761783
Err(e) => {
784+
// Check for fatal errors (network, auth) before retrying
785+
if let Some(pattern) = check_fatal_error(&e) {
786+
catch_error_and_exit(&format!(
787+
"Fatal error (matched '{}'):\n\n{}\n",
788+
pattern, e
789+
));
790+
}
762791
if !ignore_errors {
763792
if attempt < retries {
764793
debug!(

0 commit comments

Comments
 (0)