tasker-rb 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/DEVELOPMENT.md +548 -0
  3. data/README.md +87 -0
  4. data/ext/tasker_core/Cargo.lock +4720 -0
  5. data/ext/tasker_core/Cargo.toml +76 -0
  6. data/ext/tasker_core/extconf.rb +38 -0
  7. data/ext/tasker_core/src/CLAUDE.md +7 -0
  8. data/ext/tasker_core/src/bootstrap.rs +320 -0
  9. data/ext/tasker_core/src/bridge.rs +400 -0
  10. data/ext/tasker_core/src/client_ffi.rs +173 -0
  11. data/ext/tasker_core/src/conversions.rs +131 -0
  12. data/ext/tasker_core/src/diagnostics.rs +57 -0
  13. data/ext/tasker_core/src/event_handler.rs +179 -0
  14. data/ext/tasker_core/src/event_publisher_ffi.rs +239 -0
  15. data/ext/tasker_core/src/ffi_logging.rs +245 -0
  16. data/ext/tasker_core/src/global_event_system.rs +16 -0
  17. data/ext/tasker_core/src/in_process_event_ffi.rs +319 -0
  18. data/ext/tasker_core/src/lib.rs +41 -0
  19. data/ext/tasker_core/src/observability_ffi.rs +339 -0
  20. data/lib/tasker_core/batch_processing/batch_aggregation_scenario.rb +85 -0
  21. data/lib/tasker_core/batch_processing/batch_worker_context.rb +238 -0
  22. data/lib/tasker_core/bootstrap.rb +394 -0
  23. data/lib/tasker_core/domain_events/base_publisher.rb +220 -0
  24. data/lib/tasker_core/domain_events/base_subscriber.rb +178 -0
  25. data/lib/tasker_core/domain_events/publisher_registry.rb +253 -0
  26. data/lib/tasker_core/domain_events/subscriber_registry.rb +152 -0
  27. data/lib/tasker_core/domain_events.rb +43 -0
  28. data/lib/tasker_core/errors/CLAUDE.md +7 -0
  29. data/lib/tasker_core/errors/common.rb +305 -0
  30. data/lib/tasker_core/errors/error_classifier.rb +61 -0
  31. data/lib/tasker_core/errors.rb +4 -0
  32. data/lib/tasker_core/event_bridge.rb +330 -0
  33. data/lib/tasker_core/handlers.rb +159 -0
  34. data/lib/tasker_core/internal.rb +31 -0
  35. data/lib/tasker_core/logger.rb +234 -0
  36. data/lib/tasker_core/models.rb +337 -0
  37. data/lib/tasker_core/observability/types.rb +158 -0
  38. data/lib/tasker_core/observability.rb +292 -0
  39. data/lib/tasker_core/registry/handler_registry.rb +453 -0
  40. data/lib/tasker_core/registry/resolver_chain.rb +258 -0
  41. data/lib/tasker_core/registry/resolvers/base_resolver.rb +90 -0
  42. data/lib/tasker_core/registry/resolvers/class_constant_resolver.rb +156 -0
  43. data/lib/tasker_core/registry/resolvers/explicit_mapping_resolver.rb +146 -0
  44. data/lib/tasker_core/registry/resolvers/method_dispatch_wrapper.rb +144 -0
  45. data/lib/tasker_core/registry/resolvers/registry_resolver.rb +229 -0
  46. data/lib/tasker_core/registry/resolvers.rb +42 -0
  47. data/lib/tasker_core/registry.rb +12 -0
  48. data/lib/tasker_core/step_handler/api.rb +48 -0
  49. data/lib/tasker_core/step_handler/base.rb +354 -0
  50. data/lib/tasker_core/step_handler/batchable.rb +50 -0
  51. data/lib/tasker_core/step_handler/decision.rb +53 -0
  52. data/lib/tasker_core/step_handler/mixins/api.rb +452 -0
  53. data/lib/tasker_core/step_handler/mixins/batchable.rb +465 -0
  54. data/lib/tasker_core/step_handler/mixins/decision.rb +252 -0
  55. data/lib/tasker_core/step_handler/mixins.rb +66 -0
  56. data/lib/tasker_core/subscriber.rb +212 -0
  57. data/lib/tasker_core/task_handler/base.rb +254 -0
  58. data/lib/tasker_core/tasker_rb.so +0 -0
  59. data/lib/tasker_core/template_discovery.rb +181 -0
  60. data/lib/tasker_core/tracing.rb +166 -0
  61. data/lib/tasker_core/types/batch_processing_outcome.rb +301 -0
  62. data/lib/tasker_core/types/client_types.rb +145 -0
  63. data/lib/tasker_core/types/decision_point_outcome.rb +177 -0
  64. data/lib/tasker_core/types/error_types.rb +72 -0
  65. data/lib/tasker_core/types/simple_message.rb +151 -0
  66. data/lib/tasker_core/types/step_context.rb +328 -0
  67. data/lib/tasker_core/types/step_handler_call_result.rb +307 -0
  68. data/lib/tasker_core/types/step_message.rb +112 -0
  69. data/lib/tasker_core/types/step_types.rb +207 -0
  70. data/lib/tasker_core/types/task_template.rb +240 -0
  71. data/lib/tasker_core/types/task_types.rb +148 -0
  72. data/lib/tasker_core/types.rb +132 -0
  73. data/lib/tasker_core/version.rb +13 -0
  74. data/lib/tasker_core/worker/CLAUDE.md +7 -0
  75. data/lib/tasker_core/worker/event_poller.rb +224 -0
  76. data/lib/tasker_core/worker/in_process_domain_event_poller.rb +271 -0
  77. data/lib/tasker_core.rb +160 -0
  78. metadata +322 -0
@@ -0,0 +1,76 @@
1
+ [package]
2
+ name = "tasker-rb"
3
+ version = "0.1.1"
4
+ edition = "2021"
5
+ description = "Ruby bindings for tasker-core: High-performance workflow orchestration"
6
+ readme = "../../../../README.md"
7
+ repository = "https://github.com/tasker-systems/tasker-core"
8
+ license = "MIT"
9
+ keywords = ["bindings", "ffi", "magnus", "ruby", "worker"]
10
+ categories = ["api-bindings", "asynchronous"]
11
+
12
+ # TAS-145: Ignore false positives from cargo-machete
13
+ [package.metadata.cargo-machete]
14
+ ignored = [
15
+ # Dev-dependencies
16
+ "tasker-core",
17
+ # Used in FFI layer but not detected by analysis
18
+ "anyhow",
19
+ "async-trait",
20
+ "dotenvy",
21
+ "once_cell",
22
+ "serde_yaml",
23
+ "sqlx",
24
+ "thiserror",
25
+ "workspace_tools",
26
+ ]
27
+
28
+ [lib]
29
+ crate-type = ["cdylib"]
30
+ name = "tasker_rb"
31
+
32
+ [dependencies]
33
+ anyhow = { workspace = true }
34
+ # Async trait support for FrameworkIntegration
35
+ async-trait = { workspace = true }
36
+ # Time handling for Ruby Time ↔ Rust DateTime conversion
37
+ chrono = { workspace = true }
38
+ # Environment variable loading
39
+ dotenvy = { workspace = true }
40
+ # Ruby FFI
41
+ # NOTE: "embed" feature removed for M4 Pro compatibility (TAS-XXX)
42
+ # Static linking with embed causes segfaults on Apple M4 Pro due to
43
+ # ARM64 FEAT_LSE2 atomic instruction incompatibility
44
+ magnus = { version = "0.8" }
45
+ # Once Cell
46
+ once_cell = { workspace = true }
47
+ # Serde for serialization/deserialization
48
+ serde = { workspace = true }
49
+ # Serialization for Ruby ↔ Rust data conversion
50
+ serde_json = { workspace = true }
51
+ serde_magnus = "0.10"
52
+ serde_yaml = { workspace = true }
53
+ # Database (needed for PgPool)
54
+ sqlx = { workspace = true }
55
+ # Core library dependency (relative path to parent)
56
+ tasker-shared = { path = "../../../../tasker-shared" }
57
+ tasker-worker = { path = "../../../../tasker-worker" }
58
+ # Error handling
59
+ thiserror = { workspace = true }
60
+ # Async runtime for blocking on futures in FFI
61
+ tokio = { workspace = true, features = ["rt-multi-thread"] }
62
+ # Logging
63
+ tracing = { workspace = true }
64
+ # UUID generation for event bridge
65
+ uuid = { workspace = true }
66
+ # Project root detection
67
+ workspace_tools = { workspace = true }
68
+
69
+ [dev-dependencies]
70
+ tasker-core = { package = "tasker-core", path = "../../../../" }
71
+
72
+ [features]
73
+ default = []
74
+
75
+ [lints]
76
+ workspace = true
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'mkmf'
5
+ require 'rb_sys/mkmf'
6
+
7
+ # Ensure we have the required tools
8
+ unless find_executable('cargo')
9
+ abort <<~MSG
10
+
11
+ ❌ Rust toolchain not found!
12
+
13
+ tasker-rb requires Rust to compile the native extension.
14
+
15
+ Please install Rust:
16
+ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
17
+ source $HOME/.cargo/env
18
+
19
+ Or visit: https://rustup.rs/
20
+
21
+ MSG
22
+ end
23
+
24
+ # Check Rust version
25
+ rust_version = `cargo --version`.strip
26
+ puts "🦀 Using Rust: #{rust_version}"
27
+
28
+ # Enable test-helpers feature when running tests
29
+ if ENV['ENABLE_TEST_HELPERS'] || ENV['TASKER_ENV'] == 'test'
30
+ ENV['CARGO_FEATURE_TEST_HELPERS'] = '1'
31
+ puts '🧪 Enabling test helpers for development/test environment'
32
+ end
33
+
34
+ # Create the Rust makefile for the extension
35
+ # This will compile the Rust code into a Ruby-loadable shared library
36
+ create_rust_makefile('tasker_rb')
37
+
38
+ puts "✅ Configuration complete! Run 'make' to compile the extension."
@@ -0,0 +1,7 @@
1
+ <claude-mem-context>
2
+ # Recent Activity
3
+
4
+ <!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
5
+
6
+ *No recent activity*
7
+ </claude-mem-context>
@@ -0,0 +1,320 @@
1
+ //! # Ruby Worker Bootstrap
2
+ //!
3
+ //! TAS-67: Follows the same patterns as workers/rust/src/bootstrap.rs but adapted
4
+ //! for Ruby FFI integration with magnus. Uses FfiDispatchChannel for step event
5
+ //! dispatch instead of the legacy RubyEventHandler.
6
+ //!
7
+ //! ## Architecture
8
+ //!
9
+ //! ```text
10
+ //! WorkerBootstrap → WorkerSystemHandle → take_dispatch_handles()
11
+ //! │
12
+ //! ▼
13
+ //! FfiDispatchChannel
14
+ //! │
15
+ //! ┌────────────────┴────────────────┐
16
+ //! ▼ ▼
17
+ //! poll_step_events() complete_step_event()
18
+ //! (Ruby FFI) (Ruby FFI)
19
+ //! ```
20
+
21
+ use crate::bridge::{RubyBridgeHandle, WORKER_SYSTEM};
22
+ use crate::global_event_system::get_global_event_system;
23
+ use magnus::{value::ReprValue, Error, ExceptionClass, Ruby, Value};
24
+ use std::sync::Arc;
25
+
26
+ /// Helper to get RuntimeError exception class
27
+ /// Uses the new magnus 0.8 API pattern
28
+ fn runtime_error_class() -> ExceptionClass {
29
+ Ruby::get()
30
+ .expect("Ruby runtime should be available")
31
+ .exception_runtime_error()
32
+ }
33
+ use tasker_worker::worker::{
34
+ services::CheckpointService, DomainEventCallback, FfiDispatchChannel, FfiDispatchChannelConfig,
35
+ StepEventPublisherRegistry,
36
+ };
37
+ use tasker_worker::WorkerBootstrap;
38
+ use tokio::sync::RwLock;
39
+ use tracing::{error, info};
40
+ use uuid::Uuid;
41
+
42
+ /// Bootstrap the worker system for Ruby
43
+ ///
44
+ /// TAS-67: This now uses FfiDispatchChannel for step event dispatch,
45
+ /// matching the architecture of the Rust worker but adapted for FFI.
46
+ ///
47
+ /// Returns a handle ID that Ruby can use to reference the worker system.
48
+ pub fn bootstrap_worker() -> Result<Value, Error> {
49
+ let worker_id = Uuid::new_v4();
50
+ let worker_id_str = format!("ruby-worker-{}", worker_id);
51
+
52
+ // Check if already running
53
+ let mut handle_guard = WORKER_SYSTEM.lock().map_err(|e| {
54
+ error!("Failed to acquire worker system lock: {}", e);
55
+ Error::new(runtime_error_class(), "Lock acquisition failed")
56
+ })?;
57
+
58
+ if handle_guard.is_some() {
59
+ // Return existing handle info
60
+ let ruby = magnus::Ruby::get().map_err(|err| {
61
+ Error::new(
62
+ runtime_error_class(),
63
+ format!("Failed to get ruby system: {}", err),
64
+ )
65
+ })?;
66
+ let hash = ruby.hash_new();
67
+ hash.aset("handle_id", worker_id.to_string())?;
68
+ hash.aset("status", "already_running")?;
69
+ hash.aset("message", "Worker system already running")?;
70
+ return Ok(hash.as_value());
71
+ }
72
+
73
+ // Create tokio runtime with explicit thread pool sizing
74
+ // NOTE: Explicit worker_threads=8 for M2/M4 Pro compatibility (TAS-XXX)
75
+ // M4 Pro has 12 cores (10 P + 2 E), M2 Pro has 8 cores (6 P + 2 E)
76
+ // Using 8 threads ensures consistent behavior across architectures
77
+ let runtime = tokio::runtime::Builder::new_multi_thread()
78
+ .worker_threads(8)
79
+ .thread_name("ruby-ffi-runtime")
80
+ .enable_all()
81
+ .build()
82
+ .map_err(|e| {
83
+ error!("Failed to create tokio runtime: {}", e);
84
+ Error::new(runtime_error_class(), "Runtime creation failed")
85
+ })?;
86
+
87
+ // TAS-65 Phase 2: Initialize telemetry in Tokio runtime context
88
+ runtime.block_on(async {
89
+ tasker_shared::logging::init_tracing();
90
+ });
91
+
92
+ // Get global event system (shared singleton)
93
+ let event_system = get_global_event_system();
94
+
95
+ // Bootstrap the worker using tasker-worker foundation
96
+ let mut system_handle = runtime.block_on(async {
97
+ WorkerBootstrap::bootstrap_with_event_system(Some(event_system))
98
+ .await
99
+ .map_err(|e| {
100
+ error!("Failed to bootstrap worker system: {}", e);
101
+ Error::new(
102
+ runtime_error_class(),
103
+ format!("Worker bootstrap failed: {}", e),
104
+ )
105
+ })
106
+ })?;
107
+
108
+ info!("✅ Worker system bootstrapped successfully");
109
+
110
+ // TAS-67: Create domain event callback for step completion
111
+ // This MUST be done BEFORE taking dispatch handles
112
+ info!("🔔 Setting up step event publisher registry for domain events...");
113
+ let (domain_event_publisher, domain_event_callback) = runtime.block_on(async {
114
+ let worker_core = system_handle.worker_core.lock().await;
115
+
116
+ // Get the message client for durable events
117
+ let message_client = worker_core.context.message_client.clone();
118
+ let publisher = Arc::new(
119
+ tasker_shared::events::domain_events::DomainEventPublisher::new(message_client),
120
+ );
121
+
122
+ // TAS-67: Get EventRouter from WorkerCore for stats tracking
123
+ // TAS-173: Use ok_or_else instead of expect to prevent panic at FFI boundary
124
+ let event_router = worker_core.event_router().ok_or_else(|| {
125
+ error!("EventRouter not available from WorkerCore after bootstrap");
126
+ Error::new(
127
+ runtime_error_class(),
128
+ "EventRouter not available from WorkerCore",
129
+ )
130
+ })?;
131
+
132
+ // Create registry with EventRouter for dual-path delivery (durable + fast)
133
+ let step_event_registry =
134
+ StepEventPublisherRegistry::with_event_router(publisher.clone(), event_router);
135
+
136
+ let registry = Arc::new(RwLock::new(step_event_registry));
137
+ let callback = Arc::new(DomainEventCallback::new(registry));
138
+
139
+ Ok::<_, Error>((publisher, callback))
140
+ })?;
141
+ info!("✅ Domain event callback created with EventRouter for stats tracking");
142
+
143
+ // TAS-67: Take dispatch handles and create FfiDispatchChannel with callback
144
+ let ffi_dispatch_channel = if let Some(dispatch_handles) = system_handle.take_dispatch_handles()
145
+ {
146
+ info!("🔗 Creating FfiDispatchChannel from dispatch handles...");
147
+
148
+ // Create config with runtime handle for executing async callbacks from FFI threads
149
+ let config = FfiDispatchChannelConfig::new(runtime.handle().clone())
150
+ .with_service_id(worker_id_str.clone())
151
+ .with_completion_timeout(std::time::Duration::from_secs(30));
152
+
153
+ // TAS-125: Get database pool for checkpoint service
154
+ // TAS-78: database_pool() returns tasker pool (backward compatible)
155
+ let db_pool = runtime.block_on(async {
156
+ let worker_core = system_handle.worker_core.lock().await;
157
+ worker_core.context.database_pool().clone()
158
+ });
159
+
160
+ // TAS-125: Create checkpoint service for batch processing handlers
161
+ let checkpoint_service = CheckpointService::new(db_pool);
162
+
163
+ let channel = FfiDispatchChannel::new(
164
+ dispatch_handles.dispatch_receiver,
165
+ dispatch_handles.completion_sender,
166
+ config,
167
+ domain_event_callback,
168
+ )
169
+ // TAS-125: Enable checkpoint support for batch processing
170
+ .with_checkpoint_support(checkpoint_service, dispatch_handles.dispatch_sender);
171
+
172
+ info!("✅ FfiDispatchChannel created with domain event callback and checkpoint support for Ruby step dispatch");
173
+ Arc::new(channel)
174
+ } else {
175
+ error!("Failed to get dispatch handles from WorkerSystemHandle");
176
+ return Err(Error::new(
177
+ runtime_error_class(),
178
+ "Dispatch handles not available",
179
+ ));
180
+ };
181
+
182
+ // TAS-65 Phase 4.1: Get in-process event receiver from WorkerCore's event bus
183
+ // IMPORTANT: We must use WorkerCore's bus, not create a new one, otherwise the
184
+ // sender is dropped when the local bus goes out of scope.
185
+ info!("⚡ Subscribing to WorkerCore's in-process event bus for fast domain events...");
186
+ let in_process_event_receiver = runtime.block_on(async {
187
+ let worker_core = system_handle.worker_core.lock().await;
188
+ // Get the in-process bus from WorkerCore and subscribe for FFI
189
+ let bus = worker_core.in_process_event_bus();
190
+ let bus_guard = bus.write().await;
191
+ bus_guard.subscribe_ffi()
192
+ });
193
+ info!("✅ Subscribed to WorkerCore's in-process event bus for FFI domain events");
194
+
195
+ // TAS-231: Create FFI client bridge for orchestration API access
196
+ info!("Creating FFI client bridge for orchestration API access...");
197
+ let ffi_client = runtime.block_on(async {
198
+ let worker_core = system_handle.worker_core.lock().await;
199
+ tasker_worker::create_ffi_client_bridge(&worker_core, runtime.handle().clone()).await
200
+ });
201
+ if ffi_client.is_some() {
202
+ info!("✅ FFI client bridge created successfully");
203
+ } else {
204
+ info!("FFI client bridge not available (orchestration client not configured)");
205
+ }
206
+
207
+ // Store the bridge handle with FfiDispatchChannel
208
+ *handle_guard = Some(RubyBridgeHandle::new(
209
+ system_handle,
210
+ ffi_dispatch_channel,
211
+ domain_event_publisher,
212
+ Some(in_process_event_receiver),
213
+ ffi_client,
214
+ runtime,
215
+ ));
216
+
217
+ // Return handle info to Ruby
218
+ let ruby = magnus::Ruby::get().map_err(|err| {
219
+ Error::new(
220
+ runtime_error_class(),
221
+ format!("Failed to get ruby system: {}", err),
222
+ )
223
+ })?;
224
+ let hash = ruby.hash_new();
225
+ hash.aset("handle_id", worker_id.to_string())?;
226
+ hash.aset("status", "started")?;
227
+ hash.aset("message", "Ruby worker system started successfully")?;
228
+ hash.aset("worker_id", worker_id_str)?;
229
+
230
+ Ok(hash.as_value())
231
+ }
232
+
233
+ /// Stop the worker system
234
+ pub fn stop_worker() -> Result<String, Error> {
235
+ let mut handle_guard = WORKER_SYSTEM.lock().map_err(|e| {
236
+ error!("Failed to acquire worker system lock: {}", e);
237
+ Error::new(runtime_error_class(), "Lock acquisition failed")
238
+ })?;
239
+
240
+ match handle_guard.as_mut() {
241
+ Some(handle) => {
242
+ handle.stop().map_err(|e| {
243
+ error!("Failed to stop worker system: {}", e);
244
+ Error::new(runtime_error_class(), e)
245
+ })?;
246
+ *handle_guard = None;
247
+ Ok("Worker system stopped".to_string())
248
+ }
249
+ None => Ok("Worker system not running".to_string()),
250
+ }
251
+ }
252
+
253
+ /// Get worker system status
254
+ pub fn get_worker_status() -> Result<Value, Error> {
255
+ let handle_guard = WORKER_SYSTEM.lock().map_err(|e| {
256
+ error!("Failed to acquire worker system lock: {}", e);
257
+ Error::new(runtime_error_class(), "Lock acquisition failed")
258
+ })?;
259
+
260
+ let ruby = magnus::Ruby::get().map_err(|err| {
261
+ error!("Failed to get ruby system: {err}");
262
+ Error::new(runtime_error_class(), "Failed to get ruby system: {err}")
263
+ })?;
264
+ let hash = ruby.hash_new();
265
+
266
+ if let Some(handle) = handle_guard.as_ref() {
267
+ let runtime = handle.runtime_handle();
268
+ let status = runtime
269
+ .block_on(async { handle.status().await })
270
+ .map_err(|err| {
271
+ error!("Failed to get status from runtime: {err}");
272
+ Error::new(
273
+ runtime_error_class(),
274
+ "Failed to get status from runtime {err}",
275
+ )
276
+ })?;
277
+
278
+ hash.aset("running", status.running)?;
279
+ hash.aset("environment", status.environment)?;
280
+ hash.aset(
281
+ "worker_core_status",
282
+ format!("{:?}", status.worker_core_status),
283
+ )?;
284
+ hash.aset("web_api_enabled", status.web_api_enabled)?;
285
+ hash.aset("supported_namespaces", status.supported_namespaces)?;
286
+ hash.aset("database_pool_size", status.database_pool_size)?;
287
+ hash.aset("database_pool_idle", status.database_pool_idle)?;
288
+ } else {
289
+ hash.aset("running", false)?;
290
+ hash.aset("error", "Worker system not initialized")?;
291
+ }
292
+
293
+ Ok(hash.as_value())
294
+ }
295
+
296
+ /// Transition to graceful shutdown
297
+ pub fn transition_to_graceful_shutdown() -> Result<String, Error> {
298
+ let handle_guard = WORKER_SYSTEM.lock().map_err(|e| {
299
+ error!("Failed to acquire worker system lock: {}", e);
300
+ Error::new(runtime_error_class(), "Lock acquisition failed")
301
+ })?;
302
+
303
+ let handle = handle_guard
304
+ .as_ref()
305
+ .ok_or_else(|| Error::new(runtime_error_class(), "Worker system not running"))?;
306
+
307
+ let runtime = handle.runtime_handle();
308
+ runtime.block_on(async {
309
+ let mut worker_core = handle.system_handle.worker_core.lock().await;
310
+ worker_core.stop().await.map_err(|e| {
311
+ error!("Failed to transition to graceful shutdown: {}", e);
312
+ Error::new(
313
+ runtime_error_class(),
314
+ format!("Graceful shutdown failed: {}", e),
315
+ )
316
+ })
317
+ })?;
318
+
319
+ Ok("Worker system transitioned to graceful shutdown".to_string())
320
+ }