1use std::collections::HashMap;
10use std::net::SocketAddr;
11use std::sync::Arc;
12use std::sync::atomic::{AtomicU8, Ordering};
13
14use dex_protocol::dex::WorkerErrorResponse;
15use dex_protocol::dex::flow_service_client::FlowServiceClient;
16use dex_protocol::dex::worker_service_server::{WorkerService, WorkerServiceServer};
17use dex_protocol::dex::{
18 InvokeExecuteMethodRequest, InvokeExecuteMethodResponse, InvokeWaitForMethodRequest,
19 InvokeWaitForMethodResponse, InvokeWorkerRpcRequest, InvokeWorkerRpcResponse,
20 SyncAttributeIndexRequest,
21};
22use prost::Message;
23use prost_types::Any;
24use tokio::runtime::Runtime;
25use tokio::sync::watch;
26use tonic::transport::{Channel, Endpoint, Server};
27use tonic::{Code, Request, Response, Status};
28
29use crate::value_hydrator::ValueHydrator;
30use crate::worker_dispatcher::WorkerDispatcher;
31use crate::{BlobCache, HandlerError, Registry, SdkError, SdkResult, WorkerOptions, WorkerTarget};
32
33const CREATED: u8 = 0;
34const RUNNING: u8 = 1;
35const STOPPED: u8 = 2;
36
37pub struct Worker {
58 runtime: Runtime,
59 service: RustWorkerService,
60 flow_service: FlowServiceClient<Channel>,
61 attribute_indexes: HashMap<String, i32>,
62 attribute_index_sync_timeout: std::time::Duration,
63 bind_address: SocketAddr,
64 worker_target: WorkerTarget,
65 shutdown: watch::Sender<bool>,
66 state: AtomicU8,
67}
68
69impl Worker {
70 pub fn new(registry: Registry, blob_cache: Arc<BlobCache>, options: WorkerOptions) -> Self {
77 Self::try_new(registry, blob_cache, options)
78 .unwrap_or_else(|error| panic!("cannot create Rust Worker: {error}"))
79 }
80
81 pub fn try_new(
88 registry: Registry,
89 blob_cache: Arc<BlobCache>,
90 options: WorkerOptions,
91 ) -> SdkResult<Self> {
92 let runtime = Runtime::new().map_err(service_error)?;
93 if options.attribute_index_sync_timeout_value().is_zero() {
94 return Err(SdkError::FlowDefinition {
95 message: "attribute index sync timeout must be positive".to_string(),
96 });
97 }
98 let bind_address = options
99 .bind_address_value()
100 .parse::<SocketAddr>()
101 .map_err(|error| SdkError::FlowDefinition {
102 message: format!("invalid Worker bind address: {error}"),
103 })?;
104 let worker_target = options.worker_target_value().cloned().unwrap_or_else(|| {
105 let host = if bind_address.ip().is_unspecified() {
106 "127.0.0.1".to_string()
107 } else {
108 bind_address.ip().to_string()
109 };
110 WorkerTarget::new(format!("{host}:{}", bind_address.port()))
111 });
112 let endpoint = Endpoint::from_shared(endpoint_address(options.server_address_value()))
113 .map_err(service_error)?;
114 let flow_service = {
115 let _runtime_guard = runtime.enter();
116 FlowServiceClient::new(endpoint.connect_lazy())
117 };
118 let attribute_indexes = registry.attribute_indexes().clone();
119 let hydrator = ValueHydrator::new(flow_service.clone(), blob_cache);
120 let dispatcher = WorkerDispatcher::new(registry, hydrator);
121 let (shutdown, _) = watch::channel(false);
122 Ok(Self {
123 runtime,
124 service: RustWorkerService { dispatcher },
125 flow_service,
126 attribute_indexes,
127 attribute_index_sync_timeout: options.attribute_index_sync_timeout_value(),
128 bind_address,
129 worker_target,
130 shutdown,
131 state: AtomicU8::new(CREATED),
132 })
133 }
134
135 pub fn worker_target(&self) -> &WorkerTarget {
137 &self.worker_target
138 }
139
140 pub fn start(&self) -> SdkResult<()> {
147 self.state
148 .compare_exchange(CREATED, RUNNING, Ordering::AcqRel, Ordering::Acquire)
149 .map_err(|_| SdkError::FlowDefinition {
150 message: "Worker can only be started once".to_string(),
151 })?;
152 let mut shutdown = self.shutdown.subscribe();
153 let mut flow_service = self.flow_service.clone();
154 let sync_result = self.runtime.block_on(async {
155 tokio::time::timeout(
156 self.attribute_index_sync_timeout,
157 flow_service.sync_attribute_indexes(SyncAttributeIndexRequest {
158 attribute_indexes: self.attribute_indexes.clone(),
159 }),
160 )
161 .await
162 });
163 match sync_result {
164 Ok(Ok(_)) => {}
165 Ok(Err(status)) => {
166 self.state.store(STOPPED, Ordering::Release);
167 return Err(service_error(status));
168 }
169 Err(elapsed) => {
170 self.state.store(STOPPED, Ordering::Release);
171 return Err(service_error(elapsed));
172 }
173 }
174 let result = self.runtime.block_on(
175 Server::builder()
176 .add_service(WorkerServiceServer::new(self.service.clone()))
177 .serve_with_shutdown(self.bind_address, async move {
178 while !*shutdown.borrow() && shutdown.changed().await.is_ok() {}
179 }),
180 );
181 self.state.store(STOPPED, Ordering::Release);
182 result.map_err(service_error)
183 }
184
185 pub fn stop(&self) {
189 let _ = self.shutdown.send(true);
190 }
191}
192
193#[derive(Clone)]
194struct RustWorkerService {
195 dispatcher: WorkerDispatcher,
196}
197
198#[tonic::async_trait]
199impl WorkerService for RustWorkerService {
200 async fn invoke_wait_for_method(
201 &self,
202 request: Request<InvokeWaitForMethodRequest>,
203 ) -> Result<Response<InvokeWaitForMethodResponse>, Status> {
204 self.dispatcher
205 .invoke_wait_for(request.into_inner())
206 .await
207 .map(Response::new)
208 .map_err(worker_status)
209 }
210
211 async fn invoke_execute_method(
212 &self,
213 request: Request<InvokeExecuteMethodRequest>,
214 ) -> Result<Response<InvokeExecuteMethodResponse>, Status> {
215 self.dispatcher
216 .invoke_execute(request.into_inner())
217 .await
218 .map(Response::new)
219 .map_err(worker_status)
220 }
221
222 async fn invoke_worker_rpc(
223 &self,
224 request: Request<InvokeWorkerRpcRequest>,
225 ) -> Result<Response<InvokeWorkerRpcResponse>, Status> {
226 self.dispatcher
227 .invoke_rpc(request.into_inner())
228 .await
229 .map(Response::new)
230 .map_err(worker_status)
231 }
232}
233
234#[derive(Clone, PartialEq, Message)]
235struct GoogleRpcStatus {
236 #[prost(int32, tag = "1")]
237 code: i32,
238 #[prost(string, tag = "2")]
239 message: String,
240 #[prost(message, repeated, tag = "3")]
241 details: Vec<Any>,
242}
243
244fn worker_status(error: HandlerError) -> Status {
245 let message = error.to_string();
246 let detail = WorkerErrorResponse {
247 detail: message.clone(),
248 error_type: error.error_type().to_string(),
249 stack_trace: String::new(),
250 retry_after_seconds: 0,
251 };
252 let status = GoogleRpcStatus {
253 code: Code::Unknown as i32,
254 message: message.clone(),
255 details: vec![Any {
256 type_url: "type.googleapis.com/dex.WorkerErrorResponse".to_string(),
257 value: detail.encode_to_vec(),
258 }],
259 };
260 Status::with_details(Code::Unknown, message, status.encode_to_vec().into())
261}
262
263fn endpoint_address(address: &str) -> String {
264 if address.contains("://") {
265 address.to_string()
266 } else {
267 format!("http://{address}")
268 }
269}
270
271fn service_error(error: impl std::fmt::Display) -> SdkError {
272 SdkError::Service {
273 service: crate::ServiceError::local("worker", error.to_string()),
274 }
275}