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::test_support::{EngineUnderTest, StateUnderTest};
491 use crate::{
492 NamespaceResolver,
493 config::{
494 AuthConfig, AuthoringConfig, DeployConfig, ListenConfig, MetricsConfig,
495 NamespaceConfig, NamespaceMode, OpsConsoleAssetSource, OpsConsoleConfig, RuntimeConfig,
496 WebSocketConfig, WorkerConfig,
497 },
498 };
499
500 const NAMESPACE: &str = "tenant-a";
501 const TOKEN: &str = "test-token";
502
503 async fn server_state(
512 engine: EngineUnderTest,
513 resolver: NamespaceResolver,
514 runtime: RuntimeConfig,
515 ) -> Result<StateUnderTest, Box<dyn std::error::Error>> {
516 #[cfg(feature = "auth")]
517 {
518 let url = crate::auth::test_support::serve_jwks()?;
519 let refresh = std::time::Duration::from_secs(runtime.auth.jwks_refresh_seconds);
520 let cache = crate::auth::JwksCache::new(url, refresh).await?;
521 Ok(StateUnderTest::over(
522 engine,
523 ServerState::from_parts_with_jwks(resolver, runtime, cache),
524 ))
525 }
526 #[cfg(not(feature = "auth"))]
527 {
528 tokio::task::yield_now().await;
530 Ok(StateUnderTest::over(
531 engine,
532 ServerState::from_parts(resolver, runtime),
533 ))
534 }
535 }
536
537 fn fixture_list_request()
540 -> Result<Request<generated::ListWorkflowsRequest>, Box<dyn std::error::Error>> {
541 let list_request = encode_core_value(
542 NAMESPACE,
543 None,
544 &aion_core::WorkflowListRequest {
545 namespace: NAMESPACE.to_owned(),
546 filter: aion_core::WorkflowListFilter {
547 workflow_types: vec![String::from("fixture")],
548 statuses: vec![WorkflowStatus::Running],
549 ..aion_core::WorkflowListFilter::default()
550 },
551 sort: aion_core::WorkflowSort {
552 field: aion_core::WorkflowSortField::StartedAt,
553 direction: aion_core::SortDirection::Desc,
554 },
555 cursor: None,
556 limit: 10,
557 },
558 )?;
559 Ok(Request::new(generated::ListWorkflowsRequest {
560 namespace: NAMESPACE.to_owned(),
561 request: Some(encode_envelope(list_request)),
562 }))
563 }
564
565 async fn seed_tenant_and_unowned_histories(
578 store: &Arc<dyn EventStore>,
579 visibility_store: &Arc<dyn VisibilityStore>,
580 ) -> Result<WorkflowId, Box<dyn std::error::Error>> {
581 let namespace_event = Event::SearchAttributesUpdated {
582 envelope: EventEnvelope {
583 seq: 2,
584 recorded_at: Utc::now(),
585 workflow_id: workflow_id(),
586 },
587 workflow_id: workflow_id(),
588 attributes: std::collections::HashMap::from([(
589 crate::namespace::NAMESPACE_ATTRIBUTE.to_owned(),
590 aion_core::SearchAttributeValue::String(NAMESPACE.to_owned()),
591 )]),
592 };
593 store
594 .append(
595 WriteToken::recorder(),
596 &workflow_id(),
597 &[started_event()?, namespace_event],
598 0,
599 )
600 .await?;
601 let unowned_id = WorkflowId::new(uuid::Uuid::from_u128(3));
602 let unowned_run = aion_core::RunId::new(uuid::Uuid::from_u128(4));
603 store
604 .append(
605 WriteToken::recorder(),
606 &unowned_id,
607 &[Event::WorkflowStarted {
608 envelope: EventEnvelope {
609 seq: 1,
610 recorded_at: Utc::now(),
611 workflow_id: unowned_id.clone(),
612 },
613 workflow_type: "fixture".to_owned(),
614 input: payload()?,
615 run_id: unowned_run.clone(),
616 parent_run_id: None,
617 parent_workflow_id: None,
618 package_version: aion_core::PackageVersion::new("a".repeat(64)),
619 }],
620 0,
621 )
622 .await?;
623 for (id, run) in [
624 (
625 workflow_id(),
626 aion_core::RunId::new(uuid::Uuid::from_u128(1)),
627 ),
628 (unowned_id.clone(), unowned_run),
629 ] {
630 aion::lifecycle::visibility::upsert_workflow_visibility(
631 Arc::clone(store),
632 Arc::clone(visibility_store),
633 &id,
634 &run,
635 )
636 .await?;
637 }
638 Ok(unowned_id)
639 }
640
641 #[tokio::test]
642 async fn in_process_tonic_start_and_list_use_shared_handlers()
643 -> Result<(), Box<dyn std::error::Error>> {
644 let backing = Arc::new(InMemoryStore::default());
645 let store: Arc<dyn EventStore> = backing.clone();
646 let visibility_store: Arc<dyn VisibilityStore> = backing;
647 let engine = EngineUnderTest::new(Arc::new(
648 EngineBuilder::new()
649 .stop_drain_timeout(std::time::Duration::from_secs(5))
650 .store_arc(Arc::clone(&store))
651 .visibility_store_arc(Arc::clone(&visibility_store))
652 .scheduler_threads(1)
653 .build()
654 .await?,
655 ));
656 let unowned_id = seed_tenant_and_unowned_histories(&store, &visibility_store).await?;
657 let resolver = NamespaceResolver::from_config(
658 crate::config::NamespaceConfig {
659 mode: NamespaceMode::SharedEngine,
660 },
661 engine.handle(),
662 );
663 let state = server_state(engine, resolver.clone(), runtime_config()).await?;
664 let service = WorkflowGrpcService::new(state.clone());
665
666 let mut start = Request::new(generated::StartWorkflowRequest {
667 namespace: NAMESPACE.to_owned(),
668 workflow_type: "missing-workflow".to_owned(),
669 input: Some(encode_payload(proto_payload()?)),
670 routing_key: None,
671 task_queue: None,
672 display_name: None,
673 });
674 apply_metadata(start.metadata_mut())?;
675 let start_error = service.start_workflow(start).await;
676 let status = start_error
677 .err()
678 .ok_or_else(|| WireError::backend("expected error"))?;
679 assert_eq!(status.code(), Code::NotFound);
680 let detail = ProtoWireError::decode(status.details())?;
681 assert_eq!(detail.error_type.as_deref(), Some("WorkflowTypeNotFound"));
682 assert!(detail.message.contains("missing-workflow"));
683
684 let mut list = fixture_list_request()?;
685 apply_metadata(list.metadata_mut())?;
686 let response = service.list_workflows(list).await?.into_inner();
687
688 let page = response
689 .page
690 .map(decode_envelope)
691 .map(|envelope| decode_core_value::<aion_core::WorkflowListPage>(&envelope))
692 .transpose()?
693 .ok_or_else(|| WireError::backend("page missing"))?;
694 assert_eq!(page.items.len(), 1);
695 assert_eq!(page.count, 1);
696 assert_eq!(page.items[0].workflow_id, workflow_id());
697 resolver
703 .verify_workflow_ownership(NAMESPACE, &workflow_id())
704 .await?;
705 assert_eq!(
706 resolver
707 .verify_workflow_ownership(NAMESPACE, &unowned_id)
708 .await
709 .err()
710 .map(|error| error.to_wire_error().code),
711 Some(WireErrorCode::NotFound)
712 );
713 Ok(())
714 }
715
716 async fn rename_fixture(
727 register_display_name: bool,
728 ) -> Result<
729 (WorkflowGrpcService, Arc<dyn EventStore>, StateUnderTest),
730 Box<dyn std::error::Error>,
731 > {
732 rename_fixture_with_terminal(register_display_name, true).await
733 }
734
735 async fn rename_fixture_with_terminal(
739 register_display_name: bool,
740 terminal: bool,
741 ) -> Result<
742 (WorkflowGrpcService, Arc<dyn EventStore>, StateUnderTest),
743 Box<dyn std::error::Error>,
744 > {
745 let backing = Arc::new(InMemoryStore::default());
746 let store: Arc<dyn EventStore> = backing.clone();
747 let visibility_store: Arc<dyn VisibilityStore> = backing;
748 let mut schema = aion_core::SearchAttributeSchema::new();
749 schema.register(
750 crate::namespace::NAMESPACE_ATTRIBUTE,
751 aion_core::SearchAttributeType::String,
752 )?;
753 if register_display_name {
754 schema.register(
755 crate::namespace::DISPLAY_NAME_ATTRIBUTE,
756 aion_core::SearchAttributeType::String,
757 )?;
758 }
759 let engine = EngineUnderTest::new(Arc::new(
760 EngineBuilder::new()
761 .stop_drain_timeout(std::time::Duration::from_secs(5))
762 .store_arc(Arc::clone(&store))
763 .visibility_store_arc(Arc::clone(&visibility_store))
764 .search_attribute_schema(schema)
765 .scheduler_threads(1)
766 .build()
767 .await?,
768 ));
769 let namespace_event = |seq: u64| Event::SearchAttributesUpdated {
770 envelope: EventEnvelope {
771 seq,
772 recorded_at: Utc::now(),
773 workflow_id: workflow_id(),
774 },
775 workflow_id: workflow_id(),
776 attributes: std::collections::HashMap::from([(
777 crate::namespace::NAMESPACE_ATTRIBUTE.to_owned(),
778 aion_core::SearchAttributeValue::String(NAMESPACE.to_owned()),
779 )]),
780 };
781 let mut events = vec![started_event()?, namespace_event(2)];
782 if terminal {
783 events.push(Event::WorkflowCompleted {
784 envelope: EventEnvelope {
785 seq: 3,
786 recorded_at: Utc::now(),
787 workflow_id: workflow_id(),
788 },
789 result: payload()?,
790 });
791 }
792 store
793 .append(WriteToken::recorder(), &workflow_id(), &events, 0)
794 .await?;
795 let resolver = NamespaceResolver::from_config(
796 crate::config::NamespaceConfig {
797 mode: NamespaceMode::SharedEngine,
798 },
799 engine.handle(),
800 );
801 let state = server_state(engine, resolver, runtime_config()).await?;
802 Ok((WorkflowGrpcService::new(state.clone()), store, state))
803 }
804
805 #[tokio::test]
809 async fn in_process_tonic_rename_records_the_name_and_supersedes_it()
810 -> Result<(), Box<dyn std::error::Error>> {
811 let (service, store, state) = rename_fixture(true).await?;
812
813 let rename = |name: &str| {
814 let mut request = Request::new(generated::RenameRequest {
815 namespace: NAMESPACE.to_owned(),
816 workflow_id: Some(generated::WorkflowId {
817 uuid: workflow_id().to_string(),
818 }),
819 run_id: None,
821 display_name: name.to_owned(),
822 });
823 apply_metadata(request.metadata_mut()).map(|()| request)
824 };
825
826 let response = service.rename(rename(" Nightly settlement ")?).await?;
828 let response = response.into_inner();
829 assert_eq!(response.display_name, "Nightly settlement");
830 assert_eq!(
831 response.run_id.map(|id| id.uuid),
832 Some(aion_core::RunId::new(uuid::Uuid::from_u128(1)).to_string())
833 );
834
835 service
836 .rename(rename("Nightly settlement (rerun)")?)
837 .await?;
838
839 let history = store.read_history(&workflow_id()).await?;
840 assert_eq!(
841 aion_core::display_name(&history).as_deref(),
842 Some("Nightly settlement (rerun)"),
843 "the latest recorded name wins"
844 );
845 let names: Vec<_> = history
846 .iter()
847 .filter_map(|event| match event {
848 Event::SearchAttributesUpdated { attributes, .. } => attributes
849 .get(aion_core::DISPLAY_NAME_ATTRIBUTE)
850 .and_then(|value| match value {
851 aion_core::SearchAttributeValue::String(name) => Some(name.clone()),
852 _ => None,
853 }),
854 _ => None,
855 })
856 .collect();
857 assert_eq!(
858 names,
859 vec![
860 String::from("Nightly settlement"),
861 String::from("Nightly settlement (rerun)")
862 ],
863 "history keeps every name the run has worn"
864 );
865 state.shutdown()?;
866 Ok(())
867 }
868
869 #[tokio::test]
872 async fn in_process_tonic_rename_refuses_a_blank_name() -> Result<(), Box<dyn std::error::Error>>
873 {
874 let (service, store, state) = rename_fixture(false).await?;
875 let before = store.read_history(&workflow_id()).await?.len();
876
877 let mut request = Request::new(generated::RenameRequest {
878 namespace: NAMESPACE.to_owned(),
879 workflow_id: Some(generated::WorkflowId {
880 uuid: workflow_id().to_string(),
881 }),
882 run_id: None,
883 display_name: String::from(" "),
884 });
885 apply_metadata(request.metadata_mut())?;
886 let status = service
887 .rename(request)
888 .await
889 .err()
890 .ok_or_else(|| WireError::backend("expected a blank-name refusal"))?;
891 assert_eq!(status.code(), Code::InvalidArgument);
892 assert_eq!(
893 store.read_history(&workflow_id()).await?.len(),
894 before,
895 "a refused rename appends nothing"
896 );
897 state.shutdown()?;
898 Ok(())
899 }
900
901 #[tokio::test]
907 async fn in_process_tonic_rename_of_a_non_resident_running_run_is_failed_precondition()
908 -> Result<(), Box<dyn std::error::Error>> {
909 let (service, store, state) = rename_fixture_with_terminal(true, false).await?;
910 let before = store.read_history(&workflow_id()).await?.len();
911
912 let mut request = Request::new(generated::RenameRequest {
913 namespace: NAMESPACE.to_owned(),
914 workflow_id: Some(generated::WorkflowId {
915 uuid: workflow_id().to_string(),
916 }),
917 run_id: None,
918 display_name: String::from("Nightly settlement"),
919 });
920 apply_metadata(request.metadata_mut())?;
921 let status = service
922 .rename(request)
923 .await
924 .err()
925 .ok_or_else(|| WireError::backend("expected a residency refusal"))?;
926
927 assert_eq!(status.code(), Code::FailedPrecondition);
928 let detail = ProtoWireError::decode(status.details())?;
929 assert_eq!(detail.error_type.as_deref(), Some("InvalidState"));
930 assert!(
931 detail.message.contains("not resident"),
932 "the refusal must name the reason: {}",
933 detail.message
934 );
935 assert_eq!(
936 store.read_history(&workflow_id()).await?.len(),
937 before,
938 "a refused rename appends nothing"
939 );
940 state.shutdown()?;
941 Ok(())
942 }
943
944 #[tokio::test]
948 async fn in_process_tonic_reopen_completed_is_failed_precondition_invalid_state()
949 -> Result<(), Box<dyn std::error::Error>> {
950 let backing = Arc::new(InMemoryStore::default());
951 let store: Arc<dyn EventStore> = backing.clone();
952 let visibility_store: Arc<dyn VisibilityStore> = backing;
953 let engine = EngineUnderTest::new(Arc::new(
954 EngineBuilder::new()
955 .stop_drain_timeout(std::time::Duration::from_secs(5))
956 .store_arc(Arc::clone(&store))
957 .visibility_store_arc(Arc::clone(&visibility_store))
958 .scheduler_threads(1)
959 .build()
960 .await?,
961 ));
962 store
966 .append(
967 WriteToken::recorder(),
968 &workflow_id(),
969 &[
970 started_event()?,
971 Event::SearchAttributesUpdated {
972 envelope: EventEnvelope {
973 seq: 2,
974 recorded_at: Utc::now(),
975 workflow_id: workflow_id(),
976 },
977 workflow_id: workflow_id(),
978 attributes: std::collections::HashMap::from([(
979 crate::namespace::NAMESPACE_ATTRIBUTE.to_owned(),
980 aion_core::SearchAttributeValue::String(NAMESPACE.to_owned()),
981 )]),
982 },
983 Event::WorkflowCompleted {
984 envelope: EventEnvelope {
985 seq: 3,
986 recorded_at: Utc::now(),
987 workflow_id: workflow_id(),
988 },
989 result: payload()?,
990 },
991 ],
992 0,
993 )
994 .await?;
995 let resolver = NamespaceResolver::from_config(
996 crate::config::NamespaceConfig {
997 mode: NamespaceMode::SharedEngine,
998 },
999 engine.handle(),
1000 );
1001 let state = server_state(engine, resolver, runtime_config()).await?;
1002 let service = WorkflowGrpcService::new(state.clone());
1003
1004 let mut reopen = Request::new(generated::ReopenRequest {
1005 namespace: NAMESPACE.to_owned(),
1006 workflow_id: Some(generated::WorkflowId {
1007 uuid: workflow_id().to_string(),
1008 }),
1009 run_id: None,
1010 });
1011 apply_metadata(reopen.metadata_mut())?;
1012 let status = service
1013 .reopen(reopen)
1014 .await
1015 .err()
1016 .ok_or_else(|| WireError::backend("expected a reopen precondition error"))?;
1017 assert_eq!(status.code(), Code::FailedPrecondition);
1018 let detail = ProtoWireError::decode(status.details())?;
1019 assert_eq!(detail.error_type.as_deref(), Some("InvalidState"));
1020 assert_eq!(
1021 detail.code,
1022 aion_proto::ProtoWireErrorCode::InvalidState as i32
1023 );
1024 Ok(())
1025 }
1026
1027 fn apply_metadata(
1028 metadata: &mut tonic::metadata::MetadataMap,
1029 ) -> Result<(), Box<dyn std::error::Error>> {
1030 #[cfg(feature = "auth")]
1034 let bearer = crate::auth::test_support::mint_token("alice", NAMESPACE)?;
1035 #[cfg(not(feature = "auth"))]
1036 let bearer = TOKEN.to_owned();
1037 metadata.insert("authorization", format!("Bearer {bearer}").parse()?);
1038 metadata.insert("x-aion-subject", "alice".parse()?);
1039 metadata.insert("x-aion-namespaces", NAMESPACE.parse()?);
1040 Ok(())
1041 }
1042
1043 fn runtime_config() -> RuntimeConfig {
1047 RuntimeConfig {
1048 listen: ListenConfig {
1049 grpc: SocketAddr::from(([127, 0, 0, 1], 50051)),
1050 http: SocketAddr::from(([127, 0, 0, 1], 8080)),
1051 },
1052 tls: None,
1053 auth: AuthConfig {
1054 enabled: true,
1055 jwks_url: Some(TOKEN.to_owned()),
1056 jwks_refresh_seconds: 300,
1057 },
1058 ops_console: OpsConsoleConfig {
1059 source: OpsConsoleAssetSource::Embedded,
1060 },
1061 namespace: NamespaceConfig {
1062 mode: NamespaceMode::SharedEngine,
1063 },
1064 worker: WorkerConfig {
1065 heartbeat_window: std::time::Duration::from_secs(30),
1066 ..WorkerConfig::default()
1067 },
1068 websocket: WebSocketConfig {
1069 outbound_buffer_bound: 32,
1070 event_broadcast_capacity: Some(64),
1071 cluster_broadcast_capacity: Some(64),
1072 },
1073 workflow_packages: Vec::new(),
1074 deploy: DeployConfig::default(),
1075 authoring: AuthoringConfig::default(),
1076 dev: crate::config::DevConfig::default(),
1077 outbox: crate::config::OutboxConfig::default(),
1078 observability: crate::config::ObservabilityConfig::with_flush_policy(64, 0),
1079 mcp: crate::config::ResolvedMcpConfig::default(),
1080 assistant: crate::config::ResolvedAssistantConfig::default(),
1081 scheduler_threads: 1,
1082 stop_drain_timeout: Some(std::time::Duration::from_secs(5)),
1083 jit_threshold: None,
1084 query_timeout: Some(std::time::Duration::from_secs(10)),
1085 workloop_sweep_interval: Some(std::time::Duration::from_millis(50)),
1086 default_namespace: "default".to_owned(),
1087 auto_create: crate::config::AutoCreate::Open,
1088 max_in_flight_activities: crate::config::DEFAULT_MAX_IN_FLIGHT_ACTIVITIES,
1089 drain_timeout: std::time::Duration::from_secs(30),
1090 metrics: MetricsConfig { enabled: true },
1091 owned_shards: Vec::new(),
1092 cors_allowed_origins: Vec::new(),
1093 }
1094 }
1095
1096 fn started_event() -> Result<Event, aion_core::PayloadError> {
1097 Ok(Event::WorkflowStarted {
1098 envelope: EventEnvelope {
1099 seq: 1,
1100 recorded_at: Utc::now(),
1101 workflow_id: workflow_id(),
1102 },
1103 workflow_type: "fixture".to_owned(),
1104 input: payload()?,
1105 run_id: aion_core::RunId::new(uuid::Uuid::from_u128(1)),
1106 parent_run_id: None,
1107 parent_workflow_id: None,
1108 package_version: aion_core::PackageVersion::new("a".repeat(64)),
1109 })
1110 }
1111
1112 fn proto_payload() -> Result<aion_proto::ProtoPayload, aion_core::PayloadError> {
1113 Ok(payload()?.into())
1114 }
1115
1116 fn payload() -> Result<Payload, aion_core::PayloadError> {
1117 Payload::from_json(&json!({ "fixture": "input" }))
1118 }
1119
1120 fn workflow_id() -> WorkflowId {
1121 WorkflowId::new(uuid::Uuid::from_u128(1))
1122 }
1123}