1mod auth;
5pub(crate) mod convert;
9mod mint_resolve;
11mod plain_rpcs;
13mod routing_resolve;
15mod status;
17
18pub(crate) use auth::caller_from_metadata;
19pub(crate) use status::{status_from_wire_error, status_with_code};
20
21use aion_proto::generated::{self, workflow_service_server::WorkflowServiceServer};
22use tonic::{Request, Response, Status};
23
24use crate::namespace::MintCredentials;
25use crate::routing::{ForwardReply, ForwardRequest};
26use crate::{CallerIdentity, ServerState, api::handlers};
27use convert::decode_workflow_id;
28use convert::{
29 decode_cancel_request, decode_pause_request, decode_query_request, decode_rename_request,
30 decode_reopen_request, decode_resume_request, decode_signal_request, decode_start_request,
31 encode_cancel_response, encode_pause_response, encode_query_response, encode_rename_response,
32 encode_reopen_response, encode_resume_response, encode_signal_response, encode_start_response,
33};
34use routing_resolve::{RouteResolution, StartResolution};
35
36#[derive(Clone)]
38pub struct WorkflowGrpcService {
39 state: ServerState,
40}
41
42impl WorkflowGrpcService {
43 #[must_use]
45 pub const fn new(state: ServerState) -> Self {
46 Self { state }
47 }
48
49 async fn caller<T>(&self, request: &Request<T>) -> Result<CallerIdentity, Status> {
50 caller_from_metadata(request.metadata(), &self.state).await
51 }
52}
53
54#[must_use]
56pub fn workflow_service(state: ServerState) -> WorkflowServiceServer<WorkflowGrpcService> {
57 WorkflowServiceServer::new(WorkflowGrpcService::new(state))
58}
59
60#[tonic::async_trait]
61impl generated::workflow_service_server::WorkflowService for WorkflowGrpcService {
62 async fn start_workflow(
63 &self,
64 request: Request<generated::StartWorkflowRequest>,
65 ) -> Result<Response<generated::StartWorkflowResponse>, Status> {
66 if self.state.drain_state().is_draining() {
67 return Err(Status::unavailable(
68 "server is draining and not accepting new workflow starts",
69 ));
70 }
71 let caller = self.caller(&request).await?;
72 let (metadata, _ext, inner) = request.into_parts();
78 let placement = match self.resolve_start(&inner, &metadata).await {
79 StartResolution::Reject(status) => return Err(status),
80 StartResolution::Reply(reply) => return Ok(Response::new(reply)),
81 StartResolution::Local(placement) => placement,
82 };
83 let minter = self
92 .state
93 .namespace_minter()
94 .with_caller_credentials(MintCredentials::from_grpc_metadata(&metadata));
95 let response = handlers::start_with_placement(
96 self.state.namespace_guard(),
97 &caller,
98 decode_start_request(inner),
99 placement,
100 Some(&minter),
101 )
102 .await
103 .map_err(status_from_wire_error)?;
104 return Ok(Response::new(encode_start_response(response)));
105 }
106
107 async fn signal(
108 &self,
109 request: Request<generated::SignalRequest>,
110 ) -> Result<Response<generated::SignalResponse>, Status> {
111 let caller = self.caller(&request).await?;
112 let (metadata, _ext, inner) = request.into_parts();
113 let workflow_id = inner.workflow_id.clone().map(decode_workflow_id);
114 match self
115 .resolve_route(
116 workflow_id,
117 &metadata,
118 ForwardRequest::Signal(inner.clone()),
119 )
120 .await
121 {
122 RouteResolution::Reject(status) => return Err(status),
123 RouteResolution::Reply(ForwardReply::Signal(reply)) => {
124 return Ok(Response::new(reply));
125 }
126 RouteResolution::Reply(_) => {
127 return Err(Status::internal("forwarder returned a mismatched reply"));
128 }
129 RouteResolution::Local => {
130 let response = handlers::signal(
131 self.state.namespace_guard(),
132 &caller,
133 decode_signal_request(inner),
134 )
135 .await
136 .map_err(status_from_wire_error)?;
137 return Ok(Response::new(encode_signal_response(response)));
138 }
139 }
140 }
141
142 async fn query(
143 &self,
144 request: Request<generated::QueryRequest>,
145 ) -> Result<Response<generated::QueryResponse>, Status> {
146 let caller = self.caller(&request).await?;
147 let (metadata, _ext, inner) = request.into_parts();
148 let workflow_id = inner.workflow_id.clone().map(decode_workflow_id);
149 match self
150 .resolve_route(workflow_id, &metadata, ForwardRequest::Query(inner.clone()))
151 .await
152 {
153 RouteResolution::Reject(status) => return Err(status),
154 RouteResolution::Reply(ForwardReply::Query(reply)) => {
155 return Ok(Response::new(reply));
156 }
157 RouteResolution::Reply(_) => {
158 return Err(Status::internal("forwarder returned a mismatched reply"));
159 }
160 RouteResolution::Local => {
161 let response = handlers::query(
162 self.state.namespace_guard(),
163 &caller,
164 decode_query_request(inner),
165 )
166 .await
167 .map_err(status_from_wire_error)?;
168 return Ok(Response::new(encode_query_response(response)));
169 }
170 }
171 }
172
173 async fn cancel(
174 &self,
175 request: Request<generated::CancelRequest>,
176 ) -> Result<Response<generated::CancelResponse>, Status> {
177 let caller = self.caller(&request).await?;
178 let (metadata, _ext, inner) = request.into_parts();
179 let workflow_id = inner.workflow_id.clone().map(decode_workflow_id);
180 match self
181 .resolve_route(
182 workflow_id,
183 &metadata,
184 ForwardRequest::Cancel(inner.clone()),
185 )
186 .await
187 {
188 RouteResolution::Reject(status) => return Err(status),
189 RouteResolution::Reply(ForwardReply::Cancel(reply)) => {
190 return Ok(Response::new(reply));
191 }
192 RouteResolution::Reply(_) => {
193 return Err(Status::internal("forwarder returned a mismatched reply"));
194 }
195 RouteResolution::Local => {
196 let response = handlers::cancel(
197 &self.state,
198 self.state.namespace_guard(),
199 &caller,
200 decode_cancel_request(inner),
201 )
202 .await
203 .map_err(status_from_wire_error)?;
204 return Ok(Response::new(encode_cancel_response(response)));
205 }
206 }
207 }
208
209 async fn retire_workloop(
213 &self,
214 request: Request<generated::RetireWorkloopRequest>,
215 ) -> Result<Response<generated::RetireWorkloopResponse>, Status> {
216 let caller = self.caller(&request).await?;
217 let (metadata, _ext, inner) = request.into_parts();
218 let workflow_id = inner.workflow_id.clone().map(decode_workflow_id);
219 match self
220 .resolve_route(
221 workflow_id,
222 &metadata,
223 ForwardRequest::RetireWorkloop(inner.clone()),
224 )
225 .await
226 {
227 RouteResolution::Reject(status) => Err(status),
228 RouteResolution::Reply(ForwardReply::RetireWorkloop(reply)) => Ok(Response::new(reply)),
229 RouteResolution::Reply(_) => {
230 Err(Status::internal("forwarder returned a mismatched reply"))
231 }
232 RouteResolution::Local => {
233 let id = inner
234 .workflow_id
235 .clone()
236 .map(decode_workflow_id)
237 .map(|id| id.uuid)
238 .ok_or_else(|| Status::invalid_argument("workflow id is required"))?;
239 let (_, reason) = handlers::retire_workloop(
240 self.state.namespace_guard(),
241 &caller,
242 inner.namespace,
243 id,
244 inner.reason,
245 )
246 .await
247 .map_err(status_from_wire_error)?;
248 Ok(Response::new(generated::RetireWorkloopResponse { reason }))
249 }
250 }
251 }
252
253 async fn reopen(
254 &self,
255 request: Request<generated::ReopenRequest>,
256 ) -> Result<Response<generated::ReopenResponse>, Status> {
257 let caller = self.caller(&request).await?;
258 let (metadata, _ext, inner) = request.into_parts();
259 let workflow_id = inner.workflow_id.clone().map(decode_workflow_id);
260 match self
261 .resolve_route(
262 workflow_id,
263 &metadata,
264 ForwardRequest::Reopen(inner.clone()),
265 )
266 .await
267 {
268 RouteResolution::Reject(status) => return Err(status),
269 RouteResolution::Reply(ForwardReply::Reopen(reply)) => {
270 return Ok(Response::new(reply));
271 }
272 RouteResolution::Reply(_) => {
273 return Err(Status::internal("forwarder returned a mismatched reply"));
274 }
275 RouteResolution::Local => {
276 let response = handlers::reopen(
277 self.state.namespace_guard(),
278 &caller,
279 decode_reopen_request(inner),
280 )
281 .await
282 .map_err(status_from_wire_error)?;
283 return Ok(Response::new(encode_reopen_response(response)));
284 }
285 }
286 }
287
288 async fn pause(
289 &self,
290 request: Request<generated::PauseRequest>,
291 ) -> Result<Response<generated::PauseResponse>, Status> {
292 let caller = self.caller(&request).await?;
293 let (metadata, _ext, inner) = request.into_parts();
294 let workflow_id = inner.workflow_id.clone().map(decode_workflow_id);
295 match self
296 .resolve_route(workflow_id, &metadata, ForwardRequest::Pause(inner.clone()))
297 .await
298 {
299 RouteResolution::Reject(status) => return Err(status),
300 RouteResolution::Reply(ForwardReply::Pause(reply)) => {
301 return Ok(Response::new(reply));
302 }
303 RouteResolution::Reply(_) => {
304 return Err(Status::internal("forwarder returned a mismatched reply"));
305 }
306 RouteResolution::Local => {
307 let response = handlers::pause(
308 self.state.namespace_guard(),
309 &caller,
310 decode_pause_request(inner),
311 )
312 .await
313 .map_err(status_from_wire_error)?;
314 return Ok(Response::new(encode_pause_response(response)));
315 }
316 }
317 }
318
319 async fn resume(
320 &self,
321 request: Request<generated::ResumeRequest>,
322 ) -> Result<Response<generated::ResumeResponse>, Status> {
323 let caller = self.caller(&request).await?;
324 let (metadata, _ext, inner) = request.into_parts();
325 let workflow_id = inner.workflow_id.clone().map(decode_workflow_id);
326 match self
327 .resolve_route(
328 workflow_id,
329 &metadata,
330 ForwardRequest::Resume(inner.clone()),
331 )
332 .await
333 {
334 RouteResolution::Reject(status) => return Err(status),
335 RouteResolution::Reply(ForwardReply::Resume(reply)) => {
336 return Ok(Response::new(reply));
337 }
338 RouteResolution::Reply(_) => {
339 return Err(Status::internal("forwarder returned a mismatched reply"));
340 }
341 RouteResolution::Local => {
342 let response = handlers::resume(
343 self.state.namespace_guard(),
344 &caller,
345 decode_resume_request(inner),
346 )
347 .await
348 .map_err(status_from_wire_error)?;
349 return Ok(Response::new(encode_resume_response(response)));
350 }
351 }
352 }
353
354 async fn rename(
355 &self,
356 request: Request<generated::RenameRequest>,
357 ) -> Result<Response<generated::RenameResponse>, Status> {
358 let caller = self.caller(&request).await?;
359 let (metadata, _ext, inner) = request.into_parts();
360 let workflow_id = inner.workflow_id.clone().map(decode_workflow_id);
361 match self
362 .resolve_route(
363 workflow_id,
364 &metadata,
365 ForwardRequest::Rename(inner.clone()),
366 )
367 .await
368 {
369 RouteResolution::Reject(status) => return Err(status),
370 RouteResolution::Reply(ForwardReply::Rename(reply)) => {
371 return Ok(Response::new(reply));
372 }
373 RouteResolution::Reply(_) => {
374 return Err(Status::internal("forwarder returned a mismatched reply"));
375 }
376 RouteResolution::Local => {
377 let response = handlers::rename(
378 self.state.namespace_guard(),
379 &caller,
380 decode_rename_request(inner),
381 )
382 .await
383 .map_err(status_from_wire_error)?;
384 return Ok(Response::new(encode_rename_response(response)));
385 }
386 }
387 }
388
389 async fn list_workflows(
390 &self,
391 request: Request<generated::ListWorkflowsRequest>,
392 ) -> Result<Response<generated::ListWorkflowsResponse>, Status> {
393 plain_rpcs::list_workflows(&self.state, request).await
394 }
395
396 async fn describe_workflow(
397 &self,
398 request: Request<generated::DescribeWorkflowRequest>,
399 ) -> Result<Response<generated::DescribeWorkflowResponse>, Status> {
400 plain_rpcs::describe_workflow(&self.state, request).await
401 }
402
403 async fn read_history(
404 &self,
405 request: Request<generated::ReadHistoryRequest>,
406 ) -> Result<Response<generated::ReadHistoryResponse>, Status> {
407 plain_rpcs::read_history(&self.state, request).await
408 }
409
410 async fn create_schedule(
411 &self,
412 request: Request<generated::CreateScheduleRequest>,
413 ) -> Result<Response<generated::CreateScheduleResponse>, Status> {
414 plain_rpcs::create_schedule(&self.state, request).await
415 }
416
417 async fn update_schedule(
418 &self,
419 request: Request<generated::UpdateScheduleRequest>,
420 ) -> Result<Response<generated::UpdateScheduleResponse>, Status> {
421 plain_rpcs::update_schedule(&self.state, request).await
422 }
423
424 async fn pause_schedule(
425 &self,
426 request: Request<generated::ScheduleIdRequest>,
427 ) -> Result<Response<generated::PauseScheduleResponse>, Status> {
428 plain_rpcs::pause_schedule(&self.state, request).await
429 }
430
431 async fn resume_schedule(
432 &self,
433 request: Request<generated::ScheduleIdRequest>,
434 ) -> Result<Response<generated::ResumeScheduleResponse>, Status> {
435 plain_rpcs::resume_schedule(&self.state, request).await
436 }
437
438 async fn delete_schedule(
439 &self,
440 request: Request<generated::ScheduleIdRequest>,
441 ) -> Result<Response<generated::DeleteScheduleResponse>, Status> {
442 plain_rpcs::delete_schedule(&self.state, request).await
443 }
444
445 async fn list_schedules(
446 &self,
447 request: Request<generated::ListSchedulesRequest>,
448 ) -> Result<Response<generated::ListSchedulesResponse>, Status> {
449 plain_rpcs::list_schedules(&self.state, request).await
450 }
451
452 async fn describe_schedule(
453 &self,
454 request: Request<generated::ScheduleIdRequest>,
455 ) -> Result<Response<generated::DescribeScheduleResponse>, Status> {
456 plain_rpcs::describe_schedule(&self.state, request).await
457 }
458
459 async fn mint_namespace(
464 &self,
465 request: Request<generated::MintNamespaceRequest>,
466 ) -> Result<Response<generated::MintNamespaceResponse>, Status> {
467 self.mint_namespace_here(request).await
468 }
469}
470
471#[cfg(test)]
472mod tests {
473 use std::{net::SocketAddr, sync::Arc};
474
475 use aion::EngineBuilder;
476 use aion_core::{Event, EventEnvelope, Payload, WorkflowId, WorkflowStatus};
477 use aion_proto::{
478 ProtoWireError, WireError, WireErrorCode,
479 convert::{decode_core_value, encode_core_value},
480 generated::workflow_service_server::WorkflowService,
481 };
482 use aion_store::{EventStore, InMemoryStore, WriteToken, visibility::VisibilityStore};
483 use chrono::Utc;
484 use prost::Message;
485 use serde_json::json;
486 use tonic::{Code, Request};
487
488 use super::convert::{decode_envelope, encode_envelope, encode_payload};
489 use super::*;
490 use crate::{
491 NamespaceResolver,
492 config::{
493 AuthConfig, AuthoringConfig, DeployConfig, ListenConfig, MetricsConfig,
494 NamespaceConfig, NamespaceMode, OpsConsoleAssetSource, OpsConsoleConfig, RuntimeConfig,
495 WebSocketConfig, WorkerConfig,
496 },
497 };
498
499 const NAMESPACE: &str = "tenant-a";
500 const TOKEN: &str = "test-token";
501
502 async fn server_state(
507 resolver: NamespaceResolver,
508 runtime: RuntimeConfig,
509 ) -> Result<ServerState, Box<dyn std::error::Error>> {
510 #[cfg(feature = "auth")]
511 {
512 let url = crate::auth::test_support::serve_jwks()?;
513 let refresh = std::time::Duration::from_secs(runtime.auth.jwks_refresh_seconds);
514 let cache = crate::auth::JwksCache::new(url, refresh).await?;
515 Ok(ServerState::from_parts_with_jwks(resolver, runtime, cache))
516 }
517 #[cfg(not(feature = "auth"))]
518 {
519 tokio::task::yield_now().await;
521 Ok(ServerState::from_parts(resolver, runtime))
522 }
523 }
524
525 fn fixture_list_request()
528 -> Result<Request<generated::ListWorkflowsRequest>, Box<dyn std::error::Error>> {
529 let list_request = encode_core_value(
530 NAMESPACE,
531 None,
532 &aion_core::WorkflowListRequest {
533 namespace: NAMESPACE.to_owned(),
534 filter: aion_core::WorkflowListFilter {
535 workflow_types: vec![String::from("fixture")],
536 statuses: vec![WorkflowStatus::Running],
537 ..aion_core::WorkflowListFilter::default()
538 },
539 sort: aion_core::WorkflowSort {
540 field: aion_core::WorkflowSortField::StartedAt,
541 direction: aion_core::SortDirection::Desc,
542 },
543 cursor: None,
544 limit: 10,
545 },
546 )?;
547 Ok(Request::new(generated::ListWorkflowsRequest {
548 namespace: NAMESPACE.to_owned(),
549 request: Some(encode_envelope(list_request)),
550 }))
551 }
552
553 async fn seed_tenant_and_unowned_histories(
566 store: &Arc<dyn EventStore>,
567 visibility_store: &Arc<dyn VisibilityStore>,
568 ) -> Result<WorkflowId, Box<dyn std::error::Error>> {
569 let namespace_event = Event::SearchAttributesUpdated {
570 envelope: EventEnvelope {
571 seq: 2,
572 recorded_at: Utc::now(),
573 workflow_id: workflow_id(),
574 },
575 workflow_id: workflow_id(),
576 attributes: std::collections::HashMap::from([(
577 crate::namespace::NAMESPACE_ATTRIBUTE.to_owned(),
578 aion_core::SearchAttributeValue::String(NAMESPACE.to_owned()),
579 )]),
580 };
581 store
582 .append(
583 WriteToken::recorder(),
584 &workflow_id(),
585 &[started_event()?, namespace_event],
586 0,
587 )
588 .await?;
589 let unowned_id = WorkflowId::new(uuid::Uuid::from_u128(3));
590 let unowned_run = aion_core::RunId::new(uuid::Uuid::from_u128(4));
591 store
592 .append(
593 WriteToken::recorder(),
594 &unowned_id,
595 &[Event::WorkflowStarted {
596 envelope: EventEnvelope {
597 seq: 1,
598 recorded_at: Utc::now(),
599 workflow_id: unowned_id.clone(),
600 },
601 workflow_type: "fixture".to_owned(),
602 input: payload()?,
603 run_id: unowned_run.clone(),
604 parent_run_id: None,
605 parent_workflow_id: None,
606 package_version: aion_core::PackageVersion::new("a".repeat(64)),
607 }],
608 0,
609 )
610 .await?;
611 for (id, run) in [
612 (
613 workflow_id(),
614 aion_core::RunId::new(uuid::Uuid::from_u128(1)),
615 ),
616 (unowned_id.clone(), unowned_run),
617 ] {
618 aion::lifecycle::visibility::upsert_workflow_visibility(
619 Arc::clone(store),
620 Arc::clone(visibility_store),
621 &id,
622 &run,
623 )
624 .await?;
625 }
626 Ok(unowned_id)
627 }
628
629 #[tokio::test]
630 async fn in_process_tonic_start_and_list_use_shared_handlers()
631 -> Result<(), Box<dyn std::error::Error>> {
632 let backing = Arc::new(InMemoryStore::default());
633 let store: Arc<dyn EventStore> = backing.clone();
634 let visibility_store: Arc<dyn VisibilityStore> = backing;
635 let engine = Arc::new(
636 EngineBuilder::new()
637 .stop_drain_timeout(std::time::Duration::from_secs(5))
638 .store_arc(Arc::clone(&store))
639 .visibility_store_arc(Arc::clone(&visibility_store))
640 .scheduler_threads(1)
641 .build()
642 .await?,
643 );
644 let unowned_id = seed_tenant_and_unowned_histories(&store, &visibility_store).await?;
645 let resolver = NamespaceResolver::from_config(
646 crate::config::NamespaceConfig {
647 mode: NamespaceMode::SharedEngine,
648 },
649 engine,
650 );
651 let state = server_state(resolver.clone(), runtime_config()).await?;
652 let service = WorkflowGrpcService::new(state);
653
654 let mut start = Request::new(generated::StartWorkflowRequest {
655 namespace: NAMESPACE.to_owned(),
656 workflow_type: "missing-workflow".to_owned(),
657 input: Some(encode_payload(proto_payload()?)),
658 routing_key: None,
659 task_queue: None,
660 display_name: None,
661 });
662 apply_metadata(start.metadata_mut())?;
663 let start_error = service.start_workflow(start).await;
664 let status = start_error
665 .err()
666 .ok_or_else(|| WireError::backend("expected error"))?;
667 assert_eq!(status.code(), Code::NotFound);
668 let detail = ProtoWireError::decode(status.details())?;
669 assert_eq!(detail.error_type.as_deref(), Some("WorkflowTypeNotFound"));
670 assert!(detail.message.contains("missing-workflow"));
671
672 let mut list = fixture_list_request()?;
673 apply_metadata(list.metadata_mut())?;
674 let response = service.list_workflows(list).await?.into_inner();
675
676 let page = response
677 .page
678 .map(decode_envelope)
679 .map(|envelope| decode_core_value::<aion_core::WorkflowListPage>(&envelope))
680 .transpose()?
681 .ok_or_else(|| WireError::backend("page missing"))?;
682 assert_eq!(page.items.len(), 1);
683 assert_eq!(page.count, 1);
684 assert_eq!(page.items[0].workflow_id, workflow_id());
685 resolver
691 .verify_workflow_ownership(NAMESPACE, &workflow_id())
692 .await?;
693 assert_eq!(
694 resolver
695 .verify_workflow_ownership(NAMESPACE, &unowned_id)
696 .await
697 .err()
698 .map(|error| error.to_wire_error().code),
699 Some(WireErrorCode::NotFound)
700 );
701 Ok(())
702 }
703
704 async fn rename_fixture(
715 register_display_name: bool,
716 ) -> Result<(WorkflowGrpcService, Arc<dyn EventStore>), Box<dyn std::error::Error>> {
717 rename_fixture_with_terminal(register_display_name, true).await
718 }
719
720 async fn rename_fixture_with_terminal(
724 register_display_name: bool,
725 terminal: bool,
726 ) -> Result<(WorkflowGrpcService, Arc<dyn EventStore>), Box<dyn std::error::Error>> {
727 let backing = Arc::new(InMemoryStore::default());
728 let store: Arc<dyn EventStore> = backing.clone();
729 let visibility_store: Arc<dyn VisibilityStore> = backing;
730 let mut schema = aion_core::SearchAttributeSchema::new();
731 schema.register(
732 crate::namespace::NAMESPACE_ATTRIBUTE,
733 aion_core::SearchAttributeType::String,
734 )?;
735 if register_display_name {
736 schema.register(
737 crate::namespace::DISPLAY_NAME_ATTRIBUTE,
738 aion_core::SearchAttributeType::String,
739 )?;
740 }
741 let engine = Arc::new(
742 EngineBuilder::new()
743 .stop_drain_timeout(std::time::Duration::from_secs(5))
744 .store_arc(Arc::clone(&store))
745 .visibility_store_arc(Arc::clone(&visibility_store))
746 .search_attribute_schema(schema)
747 .scheduler_threads(1)
748 .build()
749 .await?,
750 );
751 let namespace_event = |seq: u64| Event::SearchAttributesUpdated {
752 envelope: EventEnvelope {
753 seq,
754 recorded_at: Utc::now(),
755 workflow_id: workflow_id(),
756 },
757 workflow_id: workflow_id(),
758 attributes: std::collections::HashMap::from([(
759 crate::namespace::NAMESPACE_ATTRIBUTE.to_owned(),
760 aion_core::SearchAttributeValue::String(NAMESPACE.to_owned()),
761 )]),
762 };
763 let mut events = vec![started_event()?, namespace_event(2)];
764 if terminal {
765 events.push(Event::WorkflowCompleted {
766 envelope: EventEnvelope {
767 seq: 3,
768 recorded_at: Utc::now(),
769 workflow_id: workflow_id(),
770 },
771 result: payload()?,
772 });
773 }
774 store
775 .append(WriteToken::recorder(), &workflow_id(), &events, 0)
776 .await?;
777 let resolver = NamespaceResolver::from_config(
778 crate::config::NamespaceConfig {
779 mode: NamespaceMode::SharedEngine,
780 },
781 engine,
782 );
783 let state = server_state(resolver, runtime_config()).await?;
784 Ok((WorkflowGrpcService::new(state), store))
785 }
786
787 #[tokio::test]
791 async fn in_process_tonic_rename_records_the_name_and_supersedes_it()
792 -> Result<(), Box<dyn std::error::Error>> {
793 let (service, store) = rename_fixture(true).await?;
794
795 let rename = |name: &str| {
796 let mut request = Request::new(generated::RenameRequest {
797 namespace: NAMESPACE.to_owned(),
798 workflow_id: Some(generated::WorkflowId {
799 uuid: workflow_id().to_string(),
800 }),
801 run_id: None,
803 display_name: name.to_owned(),
804 });
805 apply_metadata(request.metadata_mut()).map(|()| request)
806 };
807
808 let response = service.rename(rename(" Nightly settlement ")?).await?;
810 let response = response.into_inner();
811 assert_eq!(response.display_name, "Nightly settlement");
812 assert_eq!(
813 response.run_id.map(|id| id.uuid),
814 Some(aion_core::RunId::new(uuid::Uuid::from_u128(1)).to_string())
815 );
816
817 service
818 .rename(rename("Nightly settlement (rerun)")?)
819 .await?;
820
821 let history = store.read_history(&workflow_id()).await?;
822 assert_eq!(
823 aion_core::display_name(&history).as_deref(),
824 Some("Nightly settlement (rerun)"),
825 "the latest recorded name wins"
826 );
827 let names: Vec<_> = history
828 .iter()
829 .filter_map(|event| match event {
830 Event::SearchAttributesUpdated { attributes, .. } => attributes
831 .get(aion_core::DISPLAY_NAME_ATTRIBUTE)
832 .and_then(|value| match value {
833 aion_core::SearchAttributeValue::String(name) => Some(name.clone()),
834 _ => None,
835 }),
836 _ => None,
837 })
838 .collect();
839 assert_eq!(
840 names,
841 vec![
842 String::from("Nightly settlement"),
843 String::from("Nightly settlement (rerun)")
844 ],
845 "history keeps every name the run has worn"
846 );
847 Ok(())
848 }
849
850 #[tokio::test]
853 async fn in_process_tonic_rename_refuses_a_blank_name() -> Result<(), Box<dyn std::error::Error>>
854 {
855 let (service, store) = rename_fixture(false).await?;
856 let before = store.read_history(&workflow_id()).await?.len();
857
858 let mut request = Request::new(generated::RenameRequest {
859 namespace: NAMESPACE.to_owned(),
860 workflow_id: Some(generated::WorkflowId {
861 uuid: workflow_id().to_string(),
862 }),
863 run_id: None,
864 display_name: String::from(" "),
865 });
866 apply_metadata(request.metadata_mut())?;
867 let status = service
868 .rename(request)
869 .await
870 .err()
871 .ok_or_else(|| WireError::backend("expected a blank-name refusal"))?;
872 assert_eq!(status.code(), Code::InvalidArgument);
873 assert_eq!(
874 store.read_history(&workflow_id()).await?.len(),
875 before,
876 "a refused rename appends nothing"
877 );
878 Ok(())
879 }
880
881 #[tokio::test]
887 async fn in_process_tonic_rename_of_a_non_resident_running_run_is_failed_precondition()
888 -> Result<(), Box<dyn std::error::Error>> {
889 let (service, store) = rename_fixture_with_terminal(true, false).await?;
890 let before = store.read_history(&workflow_id()).await?.len();
891
892 let mut request = Request::new(generated::RenameRequest {
893 namespace: NAMESPACE.to_owned(),
894 workflow_id: Some(generated::WorkflowId {
895 uuid: workflow_id().to_string(),
896 }),
897 run_id: None,
898 display_name: String::from("Nightly settlement"),
899 });
900 apply_metadata(request.metadata_mut())?;
901 let status = service
902 .rename(request)
903 .await
904 .err()
905 .ok_or_else(|| WireError::backend("expected a residency refusal"))?;
906
907 assert_eq!(status.code(), Code::FailedPrecondition);
908 let detail = ProtoWireError::decode(status.details())?;
909 assert_eq!(detail.error_type.as_deref(), Some("InvalidState"));
910 assert!(
911 detail.message.contains("not resident"),
912 "the refusal must name the reason: {}",
913 detail.message
914 );
915 assert_eq!(
916 store.read_history(&workflow_id()).await?.len(),
917 before,
918 "a refused rename appends nothing"
919 );
920 Ok(())
921 }
922
923 #[tokio::test]
927 async fn in_process_tonic_reopen_completed_is_failed_precondition_invalid_state()
928 -> Result<(), Box<dyn std::error::Error>> {
929 let backing = Arc::new(InMemoryStore::default());
930 let store: Arc<dyn EventStore> = backing.clone();
931 let visibility_store: Arc<dyn VisibilityStore> = backing;
932 let engine = Arc::new(
933 EngineBuilder::new()
934 .stop_drain_timeout(std::time::Duration::from_secs(5))
935 .store_arc(Arc::clone(&store))
936 .visibility_store_arc(Arc::clone(&visibility_store))
937 .scheduler_threads(1)
938 .build()
939 .await?,
940 );
941 store
945 .append(
946 WriteToken::recorder(),
947 &workflow_id(),
948 &[
949 started_event()?,
950 Event::SearchAttributesUpdated {
951 envelope: EventEnvelope {
952 seq: 2,
953 recorded_at: Utc::now(),
954 workflow_id: workflow_id(),
955 },
956 workflow_id: workflow_id(),
957 attributes: std::collections::HashMap::from([(
958 crate::namespace::NAMESPACE_ATTRIBUTE.to_owned(),
959 aion_core::SearchAttributeValue::String(NAMESPACE.to_owned()),
960 )]),
961 },
962 Event::WorkflowCompleted {
963 envelope: EventEnvelope {
964 seq: 3,
965 recorded_at: Utc::now(),
966 workflow_id: workflow_id(),
967 },
968 result: payload()?,
969 },
970 ],
971 0,
972 )
973 .await?;
974 let resolver = NamespaceResolver::from_config(
975 crate::config::NamespaceConfig {
976 mode: NamespaceMode::SharedEngine,
977 },
978 engine,
979 );
980 let state = server_state(resolver, runtime_config()).await?;
981 let service = WorkflowGrpcService::new(state);
982
983 let mut reopen = Request::new(generated::ReopenRequest {
984 namespace: NAMESPACE.to_owned(),
985 workflow_id: Some(generated::WorkflowId {
986 uuid: workflow_id().to_string(),
987 }),
988 run_id: None,
989 });
990 apply_metadata(reopen.metadata_mut())?;
991 let status = service
992 .reopen(reopen)
993 .await
994 .err()
995 .ok_or_else(|| WireError::backend("expected a reopen precondition error"))?;
996 assert_eq!(status.code(), Code::FailedPrecondition);
997 let detail = ProtoWireError::decode(status.details())?;
998 assert_eq!(detail.error_type.as_deref(), Some("InvalidState"));
999 assert_eq!(
1000 detail.code,
1001 aion_proto::ProtoWireErrorCode::InvalidState as i32
1002 );
1003 Ok(())
1004 }
1005
1006 fn apply_metadata(
1007 metadata: &mut tonic::metadata::MetadataMap,
1008 ) -> Result<(), Box<dyn std::error::Error>> {
1009 #[cfg(feature = "auth")]
1013 let bearer = crate::auth::test_support::mint_token("alice", NAMESPACE)?;
1014 #[cfg(not(feature = "auth"))]
1015 let bearer = TOKEN.to_owned();
1016 metadata.insert("authorization", format!("Bearer {bearer}").parse()?);
1017 metadata.insert("x-aion-subject", "alice".parse()?);
1018 metadata.insert("x-aion-namespaces", NAMESPACE.parse()?);
1019 Ok(())
1020 }
1021
1022 fn runtime_config() -> RuntimeConfig {
1026 RuntimeConfig {
1027 listen: ListenConfig {
1028 grpc: SocketAddr::from(([127, 0, 0, 1], 50051)),
1029 http: SocketAddr::from(([127, 0, 0, 1], 8080)),
1030 },
1031 tls: None,
1032 auth: AuthConfig {
1033 enabled: true,
1034 jwks_url: Some(TOKEN.to_owned()),
1035 jwks_refresh_seconds: 300,
1036 },
1037 ops_console: OpsConsoleConfig {
1038 source: OpsConsoleAssetSource::Embedded,
1039 },
1040 namespace: NamespaceConfig {
1041 mode: NamespaceMode::SharedEngine,
1042 },
1043 worker: WorkerConfig {
1044 heartbeat_window: std::time::Duration::from_secs(30),
1045 ..WorkerConfig::default()
1046 },
1047 websocket: WebSocketConfig {
1048 outbound_buffer_bound: 32,
1049 event_broadcast_capacity: Some(64),
1050 cluster_broadcast_capacity: Some(64),
1051 },
1052 workflow_packages: Vec::new(),
1053 deploy: DeployConfig::default(),
1054 authoring: AuthoringConfig::default(),
1055 dev: crate::config::DevConfig::default(),
1056 outbox: crate::config::OutboxConfig::default(),
1057 observability: crate::config::ObservabilityConfig::with_flush_policy(64, 0),
1058 mcp: crate::config::ResolvedMcpConfig::default(),
1059 assistant: crate::config::ResolvedAssistantConfig::default(),
1060 scheduler_threads: 1,
1061 stop_drain_timeout: Some(std::time::Duration::from_secs(5)),
1062 jit_threshold: None,
1063 query_timeout: Some(std::time::Duration::from_secs(10)),
1064 workloop_sweep_interval: Some(std::time::Duration::from_millis(50)),
1065 default_namespace: "default".to_owned(),
1066 auto_create: crate::config::AutoCreate::Open,
1067 max_in_flight_activities: crate::config::DEFAULT_MAX_IN_FLIGHT_ACTIVITIES,
1068 drain_timeout: std::time::Duration::from_secs(30),
1069 metrics: MetricsConfig { enabled: true },
1070 owned_shards: Vec::new(),
1071 cors_allowed_origins: Vec::new(),
1072 }
1073 }
1074
1075 fn started_event() -> Result<Event, aion_core::PayloadError> {
1076 Ok(Event::WorkflowStarted {
1077 envelope: EventEnvelope {
1078 seq: 1,
1079 recorded_at: Utc::now(),
1080 workflow_id: workflow_id(),
1081 },
1082 workflow_type: "fixture".to_owned(),
1083 input: payload()?,
1084 run_id: aion_core::RunId::new(uuid::Uuid::from_u128(1)),
1085 parent_run_id: None,
1086 parent_workflow_id: None,
1087 package_version: aion_core::PackageVersion::new("a".repeat(64)),
1088 })
1089 }
1090
1091 fn proto_payload() -> Result<aion_proto::ProtoPayload, aion_core::PayloadError> {
1092 Ok(payload()?.into())
1093 }
1094
1095 fn payload() -> Result<Payload, aion_core::PayloadError> {
1096 Payload::from_json(&json!({ "fixture": "input" }))
1097 }
1098
1099 fn workflow_id() -> WorkflowId {
1100 WorkflowId::new(uuid::Uuid::from_u128(1))
1101 }
1102}