Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
34 changes: 32 additions & 2 deletions docs/rust-server-port.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ CLI and Waterline use the first group.
- **Used by the CLI or Waterline.**
- Task queue build IDs.
- Namespace update, delete and external storage.
- `system` repair, retention and activity-timeout passes, metrics and
operator dashboards.
- `system` repair, retention and activity-timeout passes, metrics, and
the operator dashboard filtered by workflow type.
- Activities (standalone).
- **Worker protocol features.**
- Worker sessions.
Expand Down Expand Up @@ -227,6 +227,36 @@ support is not evidence of multi-node safety.
Newest checkpoints first. Each entry is exact to the source, tree and runs it
names, including unsuccessful observations.

October 21, operator dashboard:

- **Found by.** Waterline 2.12.0's landing page in front of native: its
`/waterline/api/stats` request reached native's
`/api/system/operator-dashboard/bounded`, which answered 501.
- **Source.** Native serves admin-only `GET /api/system/operator-dashboard`
and `/bounded`, in the default or a named namespace, by the rules of
Workflow 2.5.5's `OperatorDashboardSummary` over the run summaries:
- the flow counts and rate, current running and failed runs, the hour's
completed and failed runs, the longest open wait and the longest
duration as summary rows, with the model's casts and appends;
- the week's ten busiest workflow types with pass rate, error count,
status breakdown and the upper-middle duration;
- PHP's attention alerts over workers and run summaries;
- hourly completed and failed counts for the last 169 hours;
- the operator metrics snapshot. The bounded variant marks the history
audits as not requested and reports capacity evidence unavailable.
- **By workflow type.** `/bounded/workflow-types` applies PHP's
`workflow_types` validation, then answers PHP's 501
`backend_capability_unavailable` for an observer that cannot filter by
type. Native does not advertise `workflow_type_dashboard_summary`, so
Waterline does not ask for it. Published PHP 2.5.14 answers with
filtered totals; porting that filter is open.
- **Verification.** `operator_dashboard` integration tests pass on SQLite,
PostgreSQL and MySQL, and the full native suite passes on SQLite. With
identical seeds, Waterline's stats from native and from published PHP
2.5.14 differ only in identifiers, timings and durations, summary
`history_size_bytes` (above) and sort-key precision. Before the scheduled
run summary correction, native also counted one run fewer.

October 21, operator metrics and health:

- **Source.** Native serves admin-only `GET /api/system/operator-metrics` and
Expand Down
1 change: 1 addition & 0 deletions rust/src/runtime/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ impl Runtime {
delegate!(describe_namespace(name: &str) -> Result<Value>);
delegate!(operator_metrics(config: &super::operator_metrics::MetricsConfig) -> Result<Value>);
delegate!(system_health(config: &super::operator_metrics::MetricsConfig) -> Result<(bool, Value)>);
delegate!(operator_dashboard(config: &super::operator_metrics::MetricsConfig, bounded: bool) -> Result<Value>);
delegate!(create_namespace(body: Value) -> Result<Value>);
delegate!(list_search_attributes() -> Result<Value>);
delegate!(
Expand Down
31 changes: 31 additions & 0 deletions rust/src/runtime/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,12 @@ pub fn router(runtime: Runtime) -> Router {
)
.route("/api/system/operator-metrics", get(operator_metrics))
.route("/api/system/health", get(system_health))
.route("/api/system/operator-dashboard", get(operator_dashboard))
.route("/api/system/operator-dashboard/bounded", get(bounded_operator_dashboard))
.route(
"/api/system/operator-dashboard/bounded/workflow-types",
get(workflow_type_operator_dashboard),
)
.route("/api/workers", get(list_workers))
.route("/api/task-queues", get(list_task_queues))
.route("/api/task-queues/{task_queue}", get(describe_task_queue))
Expand Down Expand Up @@ -491,6 +497,9 @@ fn named_namespace_operation(method: &str, path: &str) -> bool {
if path == "/api/workers"
|| path == "/api/system/operator-metrics"
|| path == "/api/system/health"
|| path == "/api/system/operator-dashboard"
|| path == "/api/system/operator-dashboard/bounded"
|| path == "/api/system/operator-dashboard/bounded/workflow-types"
{
return method == "GET";
}
Expand Down Expand Up @@ -1075,6 +1084,28 @@ async fn operator_metrics(Extension(runtime): Extension<Runtime>) -> Result<Json
))
}

async fn operator_dashboard(Extension(runtime): Extension<Runtime>) -> Result<Json<Value>> {
Ok(Json(runtime.operator_dashboard(&metrics_config(&runtime), false).await?))
}

async fn bounded_operator_dashboard(Extension(runtime): Extension<Runtime>) -> Result<Json<Value>> {
Ok(Json(runtime.operator_dashboard(&metrics_config(&runtime), true).await?))
}

/// PHP validates `workflow_types` and then answers 501 when its observer
/// cannot filter dashboard totals by workflow type; native cannot either.
async fn workflow_type_operator_dashboard(
Query(query): Query<std::collections::HashMap<String, String>>,
) -> Result<Response> {
super::operator_dashboard::workflow_types(query.get("workflow_types").map(String::as_str))?;
Ok((
StatusCode::NOT_IMPLEMENTED,
Json(json!({"message":"The installed workflow observer cannot filter dashboard totals by workflow type.",
"reason":"backend_capability_unavailable","capability":"workflow_type_dashboard"})),
)
.into_response())
}

/// PHP answers 503 while a check reports an error.
async fn system_health(Extension(runtime): Extension<Runtime>) -> Result<Response> {
let (unavailable, body) = runtime.system_health(&metrics_config(&runtime)).await?;
Expand Down
1 change: 1 addition & 0 deletions rust/src/runtime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ mod memo_limits;
mod memos;
pub mod mysql;
mod namespaces;
mod operator_dashboard;
mod operator_metrics;
mod poll_readiness;
pub mod postgres;
Expand Down
57 changes: 57 additions & 0 deletions rust/src/runtime/operator_dashboard.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//! Request rules for published PHP's operator dashboard by workflow type.
//! The dashboard itself is built with the operator metrics.
use super::{RuntimeError, laravel_validation::failure};

/// `workflowTypeOperatorDashboard`'s validation: a JSON list of at most 4096
/// encoded bytes whose items are nonempty strings of at most 255 characters.
pub(super) fn workflow_types(value: Option<&str>) -> Result<Vec<String>, RuntimeError> {
let refuse = |message: &'static str| failure(vec![("workflow_types", vec![message])]).expect("one message");
let text = value.map(str::trim).filter(|text| !text.is_empty())
.ok_or_else(|| refuse("The workflow types field is required."))?;
if text.chars().count() > 4096 {
return Err(refuse("The workflow types field must not be greater than 4096 characters."));
}
let decoded: serde_json::Value =
serde_json::from_str(text).map_err(|_| refuse("The workflow types field must be a valid JSON string."))?;
let encoded_len = url_encoded_len(text);
let Some(items) = decoded.as_array().filter(|_| encoded_len <= 4096) else {
return Err(refuse("Provide a JSON list of workflow types with at most 4096 encoded bytes."));
};
items
.iter()
.map(|item| {
item.as_str()
.filter(|kind| !kind.is_empty() && kind.chars().count() <= 255)
.map(str::to_owned)
.ok_or_else(|| refuse("Workflow types must be nonempty strings of at most 255 characters."))
})
.collect()
}

/// `strlen(rawurlencode($value))`.
fn url_encoded_len(text: &str) -> usize {
text.bytes()
.map(|byte| if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'~') { 1 } else { 3 })
.sum()
}

#[cfg(test)]
mod tests {
use super::*;

fn message(value: Option<&str>) -> String {
match workflow_types(value) {
Err(RuntimeError::Protocol { response, .. }) => response["message"].as_str().unwrap().to_owned(),
other => panic!("{other:?}"),
}
}

#[test]
fn workflow_types_follow_php_rules() {
assert_eq!(workflow_types(Some(r#"["orders","billing"]"#)).unwrap(), vec!["orders", "billing"]);
assert_eq!(message(None), "The workflow types field is required.");
assert_eq!(message(Some("{")), "The workflow types field must be a valid JSON string.");
assert_eq!(message(Some(r#"{"a":1}"#)), "Provide a JSON list of workflow types with at most 4096 encoded bytes.");
assert_eq!(message(Some(r#"["",1]"#)), "Workflow types must be nonempty strings of at most 255 characters.");
}
}
Loading
Loading