1//! Every way a `jev*` call fails, and the one place each becomes a 2//! Postgres ERROR with a SQLSTATE a caller can branch on. The request id 3//! and attempt count go in DETAIL, so they are never lost to a message 4//! rewrite (contract *Execution*: every error carries the request id). 5 6use jev_client::{ClientError, TransportErrorKind}; 7use jev_protocol::ApiErrorKind; 8use pgrx::PgSqlErrorCode::{self, *}; 9use pgrx::PgLogLevel; 10use pgrx::pg_sys::panic::ErrorReport; 11 12pub enum Failure { 13 /// A setting is missing or unusable. 14 Setting(String), 15 /// No API key, or two. 16 Key(String), 17 /// The call itself. 18 Client(ClientError), 19 /// A `jev*` call outside the batch scan, which would run per row. 20 Unbatched(&'static str), 21 /// Over `jev.max_rows` or `jev.max_cost`, before sending. 22 Budget(String), 23 /// Reading or writing `jev_cache`. 24 Cache(String), 25 /// The equal judgment a call waited on was never answered. 26 Shared, 27 /// Under `jev.cache_only`, a judgment the cache has no answer for. 28 CacheOnly(String), 29 /// An EvalPlanQual recheck of a row version the statement did not 30 /// judge (a concurrent update changed what the call reads). 31 Recheck(String), 32} 33 34impl Failure { 35 pub fn report(self) -> ErrorReport { 36 let (code, message, detail, hint): (_, _, Option<String>, Option<&str>) = match self { 37 Failure::Setting(m) => (ERRCODE_INVALID_PARAMETER_VALUE, m, None, None), 38 Failure::Key(m) => (ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION, m, None, None), 39 Failure::Client(e) => (code(&e), e.to_string(), detail(&e), None), 40 Failure::Unbatched(function) => ( 41 ERRCODE_FEATURE_NOT_SUPPORTED, 42 format!("{function} is evaluated only by the jev scan, and this call is outside it"), 43 Some("Here it would send one request per row, one at a time.".into()), 44 Some("Use it in the WHERE clause, or the select list of a query over one relation, and not inside an aggregate or window function."), 45 ), 46 Failure::Budget(m) => ( 47 ERRCODE_PROGRAM_LIMIT_EXCEEDED, 48 m, 49 Some("Nothing more was sent.".into()), 50 Some("Filter the rows further, add a LIMIT, or raise jev.max_rows or jev.max_cost."), 51 ), 52 Failure::Cache(m) => (ERRCODE_INTERNAL_ERROR, format!("jev_cache: {m}"), None, None), 53 Failure::CacheOnly(question) => ( 54 ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE, 55 format!("jev.cache_only is on and the cache has no answer to {question:?} for this row"), 56 Some("Nothing was sent.".into()), 57 Some("Turn jev.cache_only off to ask Jev, or change jev.model or jev.cache_namespace back to the ones the answers were stored under."), 58 ), 59 Failure::Recheck(question) => ( 60 ERRCODE_T_R_SERIALIZATION_FAILURE, 61 format!("a concurrent update changed a row this statement judged {question:?} about"), 62 Some("The row's new version was not judged, and a recheck sends nothing.".into()), 63 Some("Retry the statement."), 64 ), 65 Failure::Shared => ( 66 ERRCODE_EXTERNAL_ROUTINE_EXCEPTION, 67 "the equal judgment this call shared a request with was not answered".into(), 68 None, 69 None, 70 ), 71 }; 72 let mut report = ErrorReport::new(code, message, "jev"); 73 if let Some(detail) = detail { 74 report = report.set_detail(detail); 75 } 76 if let Some(hint) = hint { 77 report = report.set_hint(hint); 78 } 79 report 80 } 81 82 /// Whether this is a failed judgment, which `jev.on_error = unsure` 83 /// answers NULL: the remote or the path to it failed on this row. 84 /// Everything that would fail every row the same way (settings, the 85 /// key, a request we built wrong, TLS, a moved model) and every guard 86 /// (the budget, `jev.cache_only`) still raises: turning those into 87 /// NULLs would hide a broken setup, or spend past a limit. 88 pub fn is_failed_judgment(&self) -> bool { 89 match self { 90 Failure::Client(e) => match e { 91 ClientError::Api { error, .. } => matches!( 92 error.kind, 93 ApiErrorKind::RequestTimeout 94 | ApiErrorKind::ContextOverflow 95 | ApiErrorKind::RateLimited 96 | ApiErrorKind::Overloaded 97 | ApiErrorKind::Server 98 ), 99 ClientError::Transport { error, .. } => matches!( 100 error.kind, 101 TransportErrorKind::NotSent 102 | TransportErrorKind::Connect 103 | TransportErrorKind::Interrupted 104 | TransportErrorKind::TooLarge 105 ), 106 ClientError::TimedOut { .. } => true, 107 // Invalid, Response, and any kind added later: raise. 108 _ => false, 109 }, 110 // The request it waited on failed; if that failure was not a 111 // failed judgment, the call that sent it raises it. 112 Failure::Shared => true, 113 Failure::Setting(_) 114 | Failure::Key(_) 115 | Failure::Unbatched(_) 116 | Failure::Budget(_) 117 | Failure::Cache(_) 118 | Failure::CacheOnly(_) 119 | Failure::Recheck(_) => false, 120 } 121 } 122 123 pub fn refusal(self) -> Box<ErrorReport> { 124 Box::new(self.report()) 125 } 126 127 pub fn raise(self) -> ! { 128 self.report().report(PgLogLevel::ERROR); 129 unreachable!("an ERROR report does not return") 130 } 131} 132 133/// Classes follow Postgres's own use: postgres_fdw reports a remote it 134/// cannot reach under 08, and a remote that failed under 38. 135fn code(e: &ClientError) -> PgSqlErrorCode { 136 match e { 137 ClientError::Invalid(_) => ERRCODE_INVALID_PARAMETER_VALUE, 138 ClientError::Api { error, .. } => match error.kind { 139 ApiErrorKind::Authentication => ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION, 140 // Too big for the model's context: a limit, not a fault. 141 ApiErrorKind::ContextOverflow => ERRCODE_PROGRAM_LIMIT_EXCEEDED, 142 // Throttled or overloaded: try later. 143 ApiErrorKind::RateLimited | ApiErrorKind::Overloaded => ERRCODE_INSUFFICIENT_RESOURCES, 144 _ => ERRCODE_EXTERNAL_ROUTINE_EXCEPTION, 145 }, 146 ClientError::Transport { error, .. } => match error.kind { 147 TransportErrorKind::Config => ERRCODE_INVALID_PARAMETER_VALUE, 148 TransportErrorKind::TooLarge => ERRCODE_PROGRAM_LIMIT_EXCEEDED, 149 TransportErrorKind::Connect | TransportErrorKind::Tls => { 150 ERRCODE_SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION 151 } 152 _ => ERRCODE_CONNECTION_FAILURE, 153 }, 154 ClientError::TimedOut { .. } => ERRCODE_CONNECTION_FAILURE, 155 ClientError::Response { .. } => ERRCODE_EXTERNAL_ROUTINE_EXCEPTION, 156 _ => ERRCODE_EXTERNAL_ROUTINE_EXCEPTION, 157 } 158} 159 160fn detail(e: &ClientError) -> Option<String> { 161 let attempts = match e { 162 ClientError::Api { attempts, .. } 163 | ClientError::Transport { attempts, .. } 164 | ClientError::TimedOut { attempts, .. } => Some(*attempts), 165 _ => None, 166 }; 167 let parts: Vec<String> = [ 168 e.request_id().map(|id| format!("request id {id}")), 169 attempts.map(|n| format!("{n} attempt{}", if n == 1 { "" } else { "s" })), 170 ] 171 .into_iter() 172 .flatten() 173 .collect(); 174 (!parts.is_empty()).then(|| parts.join("; ")) 175}