numaflow_ruby 0.1.0
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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +5 -0
- data/Cargo.lock +1863 -0
- data/Cargo.toml +11 -0
- data/README.md +31 -0
- data/Rakefile +22 -0
- data/ext/numaflow_ruby/Cargo.toml +27 -0
- data/ext/numaflow_ruby/extconf.rb +6 -0
- data/ext/numaflow_ruby/src/lib.rs +34 -0
- data/ext/numaflow_ruby/src/map.rs +263 -0
- data/ext/numaflow_ruby/src/spec_helper.rs +137 -0
- data/ext/numaflow_ruby/src/time.rs +18 -0
- data/lib/numaflow_ruby/version.rb +5 -0
- data/lib/numaflow_ruby.rb +24 -0
- data/sig/numaflow_ruby.rbs +4 -0
- metadata +91 -0
data/Cargo.toml
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# This Cargo.toml is here to let externals tools (IDEs, etc.) know that this is
|
2
|
+
# a Rust project. Your extensions dependencies should be added to the Cargo.toml
|
3
|
+
# in the ext/ directory.
|
4
|
+
|
5
|
+
[workspace]
|
6
|
+
members = ["./ext/numaflow_ruby"]
|
7
|
+
resolver = "2"
|
8
|
+
|
9
|
+
|
10
|
+
[profile.release]
|
11
|
+
debug = true
|
data/README.md
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# NumaflowRuby
|
2
|
+
|
3
|
+
TODO: Delete this and the text below, and describe your gem
|
4
|
+
|
5
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/numaflow_ruby`. To experiment with that code, run `bin/console` for an interactive prompt.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
|
10
|
+
|
11
|
+
Install the gem and add to the application's Gemfile by executing:
|
12
|
+
|
13
|
+
$ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
|
14
|
+
|
15
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
16
|
+
|
17
|
+
$ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Development
|
24
|
+
|
25
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
26
|
+
|
27
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
28
|
+
|
29
|
+
## Contributing
|
30
|
+
|
31
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/numaflow_ruby.
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rspec/core/rake_task"
|
5
|
+
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
7
|
+
|
8
|
+
require "rubocop/rake_task"
|
9
|
+
|
10
|
+
RuboCop::RakeTask.new
|
11
|
+
|
12
|
+
require "rb_sys/extensiontask"
|
13
|
+
|
14
|
+
task build: :compile
|
15
|
+
|
16
|
+
GEMSPEC = Gem::Specification.load("numaflow_ruby.gemspec")
|
17
|
+
|
18
|
+
RbSys::ExtensionTask.new("numaflow_ruby", GEMSPEC) do |ext|
|
19
|
+
ext.lib_dir = "lib/numaflow_ruby"
|
20
|
+
end
|
21
|
+
|
22
|
+
task default: %i[compile spec rubocop]
|
@@ -0,0 +1,27 @@
|
|
1
|
+
[package]
|
2
|
+
name = "numaflow_ruby"
|
3
|
+
version = "0.1.0"
|
4
|
+
edition = "2021"
|
5
|
+
authors = ["Kyle Cooke <kyle@playerdata.com>"]
|
6
|
+
publish = false
|
7
|
+
|
8
|
+
[lib]
|
9
|
+
crate-type = ["cdylib"]
|
10
|
+
|
11
|
+
[dependencies]
|
12
|
+
magnus = { version = "0.7" }
|
13
|
+
rb-sys = "0.9.116"
|
14
|
+
numaflow = { git = "https://github.com/PlayerData/numaflow-rs.git" , branch = "main" }
|
15
|
+
tokio = { version = "^1.45.1" , features = ["rt"]}
|
16
|
+
tonic = "0.13.1"
|
17
|
+
chrono = "0.4.41"
|
18
|
+
serde = { version = "1.0.219", features = ["derive"] }
|
19
|
+
serde_magnus = { version = "0.9" }
|
20
|
+
log = "0.4.27"
|
21
|
+
env_logger = "0.11.8"
|
22
|
+
simple_logger = "5.0.0"
|
23
|
+
|
24
|
+
tower = "0.5.2"
|
25
|
+
hyper-util = "0.1.16"
|
26
|
+
prost-types = "0.13.5"
|
27
|
+
tokio-stream = "0.1.17"
|
@@ -0,0 +1,34 @@
|
|
1
|
+
use magnus::{prelude::*, Error};
|
2
|
+
use simple_logger::SimpleLogger;
|
3
|
+
|
4
|
+
mod map;
|
5
|
+
mod spec_helper;
|
6
|
+
mod time;
|
7
|
+
|
8
|
+
#[magnus::init]
|
9
|
+
fn init(ruby: &magnus::Ruby) -> Result<(), Error> {
|
10
|
+
// env_logger::Builder::from_default_env().target(env_logger::Target::Stdout).build();
|
11
|
+
SimpleLogger::new().init().unwrap();
|
12
|
+
|
13
|
+
let module = ruby.define_module("NumaflowRuby")?;
|
14
|
+
module.define_singleton_method(
|
15
|
+
"rust_start_map_server",
|
16
|
+
magnus::function!(map::start_map_server, 0),
|
17
|
+
)?;
|
18
|
+
module.define_singleton_method(
|
19
|
+
"rust_get_next_message",
|
20
|
+
magnus::function!(map::get_next_message, 1),
|
21
|
+
)?;
|
22
|
+
module.define_class("MessageResponder", ruby.class_object())?;
|
23
|
+
module.define_singleton_method(
|
24
|
+
"rust_respond_to_message",
|
25
|
+
magnus::function!(map::respond_to_message, 2),
|
26
|
+
)?;
|
27
|
+
module.define_class("MessageSource", ruby.class_object())?;
|
28
|
+
module.define_singleton_method(
|
29
|
+
"rust_send_message_get_response",
|
30
|
+
magnus::function!(spec_helper::send_message_get_response, 3),
|
31
|
+
)?;
|
32
|
+
log::trace!("Magnus module NumaflowRuby initialized");
|
33
|
+
Ok(())
|
34
|
+
}
|
@@ -0,0 +1,263 @@
|
|
1
|
+
use crate::time::time_into_ruby_value;
|
2
|
+
use magnus::{IntoValue, RArray, RHash, Ruby, Value};
|
3
|
+
use numaflow::map::{MapRequest, Message, Server};
|
4
|
+
use rb_sys::rb_thread_call_without_gvl;
|
5
|
+
use std::env;
|
6
|
+
use std::ops::DerefMut;
|
7
|
+
use std::sync::Mutex;
|
8
|
+
use tokio::runtime::Runtime;
|
9
|
+
|
10
|
+
type Request = (MapRequest, tokio::sync::oneshot::Sender<Vec<Message>>);
|
11
|
+
struct ResponseValue(magnus::Value);
|
12
|
+
impl TryFrom<ResponseValue> for Message {
|
13
|
+
type Error = magnus::Error;
|
14
|
+
|
15
|
+
fn try_from(value: ResponseValue) -> Result<Self, Self::Error> {
|
16
|
+
log::trace!("attempting conversion of value: {:?}", value.0);
|
17
|
+
let Some(hash) = RHash::from_value(value.0) else {
|
18
|
+
return Err(magnus::Error::new(
|
19
|
+
magnus::exception::type_error(),
|
20
|
+
"returned value must be Array of Hash or Hash",
|
21
|
+
));
|
22
|
+
};
|
23
|
+
let tags = hash.lookup2(magnus::symbol::Symbol::new("tags"), None::<String>)?;
|
24
|
+
let keys = hash.lookup2(magnus::symbol::Symbol::new("keys"), None::<String>)?;
|
25
|
+
let value = hash.fetch(magnus::symbol::Symbol::new("value"))?;
|
26
|
+
|
27
|
+
Ok(Message { keys, value, tags })
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
impl IntoValue for MyMapRequest {
|
32
|
+
fn into_value_with(self, handle: &Ruby) -> Value {
|
33
|
+
let out = RHash::new();
|
34
|
+
out.aset("value", self.0.value.into_value_with(handle))
|
35
|
+
.unwrap();
|
36
|
+
out.aset(
|
37
|
+
"keys",
|
38
|
+
RArray::from_vec(self.0.keys).into_value_with(handle),
|
39
|
+
)
|
40
|
+
.unwrap();
|
41
|
+
if let Ok(t) = time_into_ruby_value(self.0.watermark) {
|
42
|
+
out.aset("watermark", t.into_value_with(handle)).unwrap();
|
43
|
+
}
|
44
|
+
if let Ok(t) = time_into_ruby_value(self.0.eventtime) {
|
45
|
+
out.aset("event_time", t.into_value_with(handle)).unwrap();
|
46
|
+
}
|
47
|
+
out.aset("headers", self.0.headers.into_value_with(handle))
|
48
|
+
.unwrap();
|
49
|
+
out.into_value()
|
50
|
+
}
|
51
|
+
}
|
52
|
+
pub struct MyMapRequest(MapRequest);
|
53
|
+
|
54
|
+
struct MessageSource {
|
55
|
+
rx: tokio::sync::mpsc::Receiver<Request>,
|
56
|
+
rt: tokio::runtime::Runtime,
|
57
|
+
cancel_tx: tokio::sync::broadcast::Sender<()>,
|
58
|
+
}
|
59
|
+
|
60
|
+
#[magnus::wrap(
|
61
|
+
class = "NumaflowRuby::MessageSource",
|
62
|
+
free_immediately,
|
63
|
+
frozen_shareable
|
64
|
+
)]
|
65
|
+
pub struct MutMessageSource(Mutex<MessageSource>);
|
66
|
+
|
67
|
+
#[magnus::wrap(
|
68
|
+
class = "NumaflowRuby::MessageResponder",
|
69
|
+
free_immediately,
|
70
|
+
frozen_shareable
|
71
|
+
)]
|
72
|
+
pub struct MessageResponder(Mutex<Option<tokio::sync::oneshot::Sender<Vec<Message>>>>);
|
73
|
+
|
74
|
+
struct MsgWait<'a> {
|
75
|
+
message_source: &'a mut MessageSource,
|
76
|
+
received_message: Option<Request>,
|
77
|
+
}
|
78
|
+
|
79
|
+
unsafe extern "C" fn wait_for_recv(
|
80
|
+
message_wait_ptr: *mut ::std::os::raw::c_void,
|
81
|
+
) -> *mut ::std::os::raw::c_void {
|
82
|
+
let message_wait = (message_wait_ptr as *mut MsgWait).as_mut().unwrap();
|
83
|
+
let mut cancel_rx = message_wait.message_source.cancel_tx.subscribe();
|
84
|
+
|
85
|
+
// Use select to wait for either receiver or cancellation
|
86
|
+
message_wait.received_message = message_wait.message_source.rt.block_on(async {
|
87
|
+
tokio::select! {
|
88
|
+
msg = message_wait.message_source.rx.recv() => msg,
|
89
|
+
_ = cancel_rx.recv() => None,
|
90
|
+
}
|
91
|
+
});
|
92
|
+
|
93
|
+
std::ptr::null_mut()
|
94
|
+
}
|
95
|
+
|
96
|
+
unsafe extern "C" fn unblock_fn(message_wait_ptr: *mut ::std::os::raw::c_void) {
|
97
|
+
let message_wait = (message_wait_ptr as *mut MsgWait).as_mut().unwrap();
|
98
|
+
let _ = message_wait.message_source.cancel_tx.send(());
|
99
|
+
}
|
100
|
+
|
101
|
+
pub fn get_next_message(
|
102
|
+
message_source: &MutMessageSource,
|
103
|
+
) -> Result<(MyMapRequest, MessageResponder), magnus::Error> {
|
104
|
+
let mut guard = message_source.0.lock().unwrap();
|
105
|
+
let message_source = guard.deref_mut();
|
106
|
+
match message_source.rx.try_recv() {
|
107
|
+
Ok(request) => {
|
108
|
+
log::trace!("Received request");
|
109
|
+
// return Ok(request.0.value);
|
110
|
+
return Ok((
|
111
|
+
MyMapRequest(request.0),
|
112
|
+
MessageResponder(Mutex::new(Some(request.1))),
|
113
|
+
));
|
114
|
+
}
|
115
|
+
Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {
|
116
|
+
log::trace!("No messages available");
|
117
|
+
}
|
118
|
+
Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => {
|
119
|
+
log::error!("Message channel closed");
|
120
|
+
return Err(magnus::Error::new(
|
121
|
+
magnus::exception::runtime_error(),
|
122
|
+
"Message channel closed",
|
123
|
+
));
|
124
|
+
}
|
125
|
+
};
|
126
|
+
let mut message_wait = MsgWait {
|
127
|
+
message_source,
|
128
|
+
received_message: None,
|
129
|
+
};
|
130
|
+
unsafe {
|
131
|
+
rb_thread_call_without_gvl(
|
132
|
+
Some(wait_for_recv),
|
133
|
+
(&mut message_wait) as *mut MsgWait as *mut ::std::os::raw::c_void,
|
134
|
+
Some(unblock_fn),
|
135
|
+
(&mut message_wait) as *mut MsgWait as *mut ::std::os::raw::c_void,
|
136
|
+
);
|
137
|
+
};
|
138
|
+
match message_wait.received_message {
|
139
|
+
Some(request) => {
|
140
|
+
log::trace!(
|
141
|
+
"Received request after blocking, rx is closed: {}",
|
142
|
+
request.1.is_closed()
|
143
|
+
);
|
144
|
+
Ok((
|
145
|
+
MyMapRequest(request.0),
|
146
|
+
MessageResponder(Mutex::new(Some(request.1))),
|
147
|
+
))
|
148
|
+
}
|
149
|
+
None => {
|
150
|
+
log::error!("No messages received after blocking");
|
151
|
+
Err(magnus::Error::new(
|
152
|
+
magnus::exception::runtime_error(),
|
153
|
+
"Message channel closed",
|
154
|
+
))
|
155
|
+
}
|
156
|
+
}
|
157
|
+
}
|
158
|
+
|
159
|
+
pub fn respond_to_message(
|
160
|
+
response: Value,
|
161
|
+
response_sender: &MessageResponder,
|
162
|
+
) -> Result<(), magnus::Error> {
|
163
|
+
log::trace!(
|
164
|
+
"respond_to_message called with response: {:?}, responder: {:?}, responder closed: {}",
|
165
|
+
response,
|
166
|
+
response_sender.0,
|
167
|
+
response_sender
|
168
|
+
.0
|
169
|
+
.lock()
|
170
|
+
.unwrap()
|
171
|
+
.as_ref()
|
172
|
+
.unwrap()
|
173
|
+
.is_closed()
|
174
|
+
);
|
175
|
+
let messages_array = RArray::to_ary(response)?;
|
176
|
+
let responses = messages_array
|
177
|
+
.into_iter()
|
178
|
+
.map(ResponseValue)
|
179
|
+
.map(Message::try_from)
|
180
|
+
.collect::<Result<Vec<_>, _>>()?;
|
181
|
+
log::trace!("Responding with messages: {responses:?}");
|
182
|
+
|
183
|
+
let sender = response_sender
|
184
|
+
.0
|
185
|
+
.lock()
|
186
|
+
.unwrap()
|
187
|
+
.take()
|
188
|
+
.ok_or(magnus::Error::new(
|
189
|
+
magnus::exception::runtime_error(),
|
190
|
+
"response sender has already been used",
|
191
|
+
))?;
|
192
|
+
sender.send(responses).map_err(|_| {
|
193
|
+
magnus::Error::new(
|
194
|
+
magnus::exception::runtime_error(),
|
195
|
+
"server had stopped waiting for response",
|
196
|
+
)
|
197
|
+
})
|
198
|
+
}
|
199
|
+
|
200
|
+
pub fn start_map_server() -> Result<MutMessageSource, magnus::Error> {
|
201
|
+
let rt = Runtime::new().expect("Failed to create Tokio runtime");
|
202
|
+
let (tx, rx) = tokio::sync::mpsc::channel::<Request>(100);
|
203
|
+
let (cancel_tx, _) = tokio::sync::broadcast::channel(1);
|
204
|
+
|
205
|
+
rt.block_on(async {
|
206
|
+
let mut server = Server::new(RubyMapServer {
|
207
|
+
message_handler: tx,
|
208
|
+
});
|
209
|
+
if let Ok(info_path) = env::var("NUMAFLOW_SERVER_INFO_FILE_PATH") {
|
210
|
+
server = server.with_server_info_file(info_path);
|
211
|
+
}
|
212
|
+
if let Ok(socket_path) = env::var("NUMAFLOW_SERVER_SOCKET_PATH") {
|
213
|
+
server = server.with_socket_file(socket_path);
|
214
|
+
}
|
215
|
+
println!("Starting map server... {server:?}");
|
216
|
+
tokio::spawn(async move {
|
217
|
+
server.start().await.expect("Failed to start map server");
|
218
|
+
});
|
219
|
+
});
|
220
|
+
|
221
|
+
Ok(MutMessageSource(Mutex::new(MessageSource {
|
222
|
+
rx,
|
223
|
+
rt,
|
224
|
+
cancel_tx,
|
225
|
+
})))
|
226
|
+
}
|
227
|
+
|
228
|
+
#[derive(Debug)]
|
229
|
+
struct RubyMapServer {
|
230
|
+
message_handler: tokio::sync::mpsc::Sender<Request>,
|
231
|
+
}
|
232
|
+
|
233
|
+
#[derive(Debug)]
|
234
|
+
struct DropDetector {
|
235
|
+
msg: &'static str,
|
236
|
+
}
|
237
|
+
impl Drop for DropDetector {
|
238
|
+
fn drop(&mut self) {
|
239
|
+
log::warn!("drop warning {}", self.msg);
|
240
|
+
}
|
241
|
+
}
|
242
|
+
|
243
|
+
#[tonic::async_trait]
|
244
|
+
impl numaflow::map::Mapper for RubyMapServer {
|
245
|
+
async fn map(&self, input: MapRequest) -> Vec<Message> {
|
246
|
+
let (responder_tx, responder_rx) = tokio::sync::oneshot::channel();
|
247
|
+
log::trace!("Received map request: {:?}", input.value);
|
248
|
+
self.message_handler
|
249
|
+
.send((input, responder_tx))
|
250
|
+
.await
|
251
|
+
.expect("Failed to send map request");
|
252
|
+
log::trace!("sent map request, waiting for response...");
|
253
|
+
let dd = DropDetector {
|
254
|
+
msg: "map call is being dropped",
|
255
|
+
};
|
256
|
+
let Ok(r) = responder_rx.await else {
|
257
|
+
log::error!("Failed to receive response for map request");
|
258
|
+
panic!("Failed to receive response for map request");
|
259
|
+
};
|
260
|
+
log::trace!("Received map response: {:?}, {:?}", r, dd);
|
261
|
+
r
|
262
|
+
}
|
263
|
+
}
|
@@ -0,0 +1,137 @@
|
|
1
|
+
use magnus::IntoValue;
|
2
|
+
use numaflow::servers::map::{self as proto, MapResponse};
|
3
|
+
use rb_sys::rb_thread_call_without_gvl;
|
4
|
+
use std::ptr::null_mut;
|
5
|
+
use std::sync::{Arc, Mutex};
|
6
|
+
use tokio::net::UnixStream;
|
7
|
+
use tokio::sync::mpsc;
|
8
|
+
use tokio_stream::wrappers::ReceiverStream;
|
9
|
+
use tonic::transport::Uri;
|
10
|
+
use tower::service_fn;
|
11
|
+
|
12
|
+
type Cancellable = tokio::sync::broadcast::Sender<()>;
|
13
|
+
type Request = (
|
14
|
+
(String, Vec<String>, Vec<u8>, Cancellable),
|
15
|
+
Option<MapResponse>,
|
16
|
+
);
|
17
|
+
|
18
|
+
pub(crate) fn send_message_get_response(
|
19
|
+
uds_socket_path: String,
|
20
|
+
keys: Vec<String>,
|
21
|
+
value: Vec<u8>,
|
22
|
+
) -> magnus::Value {
|
23
|
+
let mut cancel_tx = tokio::sync::broadcast::channel(1).0;
|
24
|
+
let request_arc: Arc<Mutex<Request>> = std::sync::Arc::new(Mutex::new((
|
25
|
+
(uds_socket_path, keys, value, cancel_tx.clone()),
|
26
|
+
None,
|
27
|
+
)));
|
28
|
+
let request_arc_ptr = Arc::into_raw(request_arc.clone()) as *mut ::std::os::raw::c_void;
|
29
|
+
unsafe {
|
30
|
+
rb_thread_call_without_gvl(
|
31
|
+
Some(run_the_async_bit),
|
32
|
+
request_arc_ptr,
|
33
|
+
Some(unblock_fn),
|
34
|
+
&mut cancel_tx as *mut Cancellable as *mut ::std::os::raw::c_void,
|
35
|
+
);
|
36
|
+
};
|
37
|
+
let Some(response) = request_arc.lock().unwrap().1.clone() else {
|
38
|
+
return None::<()>.into_value();
|
39
|
+
};
|
40
|
+
let mut out = Vec::new();
|
41
|
+
for response in response.results.iter() {
|
42
|
+
let response_value = magnus::RHash::new();
|
43
|
+
response_value
|
44
|
+
.aset("keys", magnus::RArray::from_vec(response.keys.clone()))
|
45
|
+
.unwrap();
|
46
|
+
response_value
|
47
|
+
.aset("value", magnus::RArray::from_vec(response.value.clone()))
|
48
|
+
.unwrap();
|
49
|
+
response_value
|
50
|
+
.aset("tags", magnus::RArray::from_vec(response.tags.clone()))
|
51
|
+
.unwrap();
|
52
|
+
out.push(response_value.into_value());
|
53
|
+
}
|
54
|
+
magnus::RArray::from_slice(&out).into_value()
|
55
|
+
}
|
56
|
+
|
57
|
+
unsafe extern "C" fn run_the_async_bit(
|
58
|
+
request_arc_ptr: *mut ::std::os::raw::c_void,
|
59
|
+
) -> *mut ::std::os::raw::c_void {
|
60
|
+
let rt = tokio::runtime::Runtime::new().unwrap();
|
61
|
+
let request_arc: Arc<Mutex<Request>> = Arc::from_raw(request_arc_ptr as _);
|
62
|
+
let mut request = request_arc.lock().unwrap();
|
63
|
+
let mut subscribe = request.0 .3.subscribe();
|
64
|
+
let response = rt.block_on(async {
|
65
|
+
tokio::select! {
|
66
|
+
resp = do_send_message_get_response(request.0.0.clone(), request.0.1.clone(), request.0.2.clone()) => Some(resp),
|
67
|
+
_ = subscribe.recv() => None,
|
68
|
+
}
|
69
|
+
});
|
70
|
+
request.1 = response;
|
71
|
+
null_mut()
|
72
|
+
}
|
73
|
+
|
74
|
+
unsafe extern "C" fn unblock_fn(cancel_ptr: *mut ::std::os::raw::c_void) {
|
75
|
+
let cancellable: &mut Cancellable = (cancel_ptr as *mut Cancellable).as_mut().unwrap();
|
76
|
+
let _ = cancellable.send(());
|
77
|
+
}
|
78
|
+
|
79
|
+
async fn do_send_message_get_response(
|
80
|
+
uds_socket_path: String,
|
81
|
+
keys: Vec<String>,
|
82
|
+
value: Vec<u8>,
|
83
|
+
) -> MapResponse {
|
84
|
+
// https://github.com/hyperium/tonic/blob/master/examples/src/uds/client.rs
|
85
|
+
let channel = tonic::transport::Endpoint::try_from("http://[::]:50051")
|
86
|
+
.unwrap()
|
87
|
+
.connect_with_connector(service_fn(move |_: Uri| {
|
88
|
+
// https://rust-lang.github.io/async-book/03_async_await/01_chapter.html#async-lifetimes
|
89
|
+
let sock_file = uds_socket_path.clone();
|
90
|
+
async move {
|
91
|
+
Ok::<_, std::io::Error>(hyper_util::rt::TokioIo::new(
|
92
|
+
UnixStream::connect(sock_file).await?,
|
93
|
+
))
|
94
|
+
}
|
95
|
+
}))
|
96
|
+
.await
|
97
|
+
.unwrap();
|
98
|
+
|
99
|
+
let mut client = numaflow::servers::map::map_client::MapClient::new(channel);
|
100
|
+
let request = proto::MapRequest {
|
101
|
+
request: Some(proto::map_request::Request {
|
102
|
+
keys,
|
103
|
+
value,
|
104
|
+
watermark: Some(prost_types::Timestamp::default()),
|
105
|
+
event_time: Some(prost_types::Timestamp::default()),
|
106
|
+
headers: Default::default(),
|
107
|
+
}),
|
108
|
+
id: "".to_string(),
|
109
|
+
handshake: None,
|
110
|
+
status: None,
|
111
|
+
};
|
112
|
+
|
113
|
+
let (tx, rx) = mpsc::channel(2);
|
114
|
+
let handshake_request = proto::MapRequest {
|
115
|
+
request: None,
|
116
|
+
id: "".to_string(),
|
117
|
+
handshake: Some(proto::Handshake { sot: true }),
|
118
|
+
status: None,
|
119
|
+
};
|
120
|
+
|
121
|
+
tx.send(handshake_request).await.unwrap();
|
122
|
+
tx.send(request).await.unwrap();
|
123
|
+
|
124
|
+
let resp = client.map_fn(ReceiverStream::new(rx)).await.unwrap();
|
125
|
+
let mut resp = resp.into_inner();
|
126
|
+
|
127
|
+
let handshake_response = resp.message().await.unwrap();
|
128
|
+
assert!(handshake_response.is_some());
|
129
|
+
|
130
|
+
let handshake_response = handshake_response.unwrap();
|
131
|
+
assert!(handshake_response.handshake.is_some());
|
132
|
+
|
133
|
+
let actual_response = resp.message().await.unwrap();
|
134
|
+
assert!(actual_response.is_some());
|
135
|
+
|
136
|
+
actual_response.unwrap()
|
137
|
+
}
|
@@ -0,0 +1,18 @@
|
|
1
|
+
use chrono::{DateTime, Utc};
|
2
|
+
use serde::Serialize;
|
3
|
+
use serde_magnus::serialize;
|
4
|
+
|
5
|
+
#[derive(Serialize)]
|
6
|
+
struct RubyTimeUtc {
|
7
|
+
sec: i64,
|
8
|
+
nsec: i32,
|
9
|
+
}
|
10
|
+
|
11
|
+
pub fn time_into_ruby_value(time: DateTime<Utc>) -> Result<magnus::Value, magnus::Error> {
|
12
|
+
let delta = time.signed_duration_since(DateTime::UNIX_EPOCH);
|
13
|
+
let ts = RubyTimeUtc {
|
14
|
+
sec: delta.num_seconds(),
|
15
|
+
nsec: delta.subsec_nanos(),
|
16
|
+
};
|
17
|
+
serialize(&ts)
|
18
|
+
}
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "numaflow_ruby/version"
|
4
|
+
require_relative "numaflow_ruby/numaflow_ruby"
|
5
|
+
|
6
|
+
# NumaflowRuby is a Ruby wrapper for Numaflow rust library, allowing you to create map servers in Ruby.
|
7
|
+
module NumaflowRuby
|
8
|
+
class Error < StandardError; end
|
9
|
+
|
10
|
+
def self.start_map_server(map_handler) # rubocop:disable Metrics/MethodLength
|
11
|
+
Thread.new do
|
12
|
+
msg_source = NumaflowRuby.rust_start_map_server
|
13
|
+
loop do
|
14
|
+
val, responder = NumaflowRuby.rust_get_next_message(msg_source)
|
15
|
+
begin
|
16
|
+
result = map_handler.call(val)
|
17
|
+
NumaflowRuby.rust_respond_to_message(result, responder)
|
18
|
+
rescue StandardError
|
19
|
+
Kernel.exit 1
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
metadata
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: numaflow_ruby
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Kyle Cooke
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-08-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rb_sys
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.9.39
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.9.39
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake-compiler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.2.0
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.2.0
|
41
|
+
description: Ruby bindings for numaflow sdk
|
42
|
+
email:
|
43
|
+
- kyle@playerdata.com
|
44
|
+
executables: []
|
45
|
+
extensions:
|
46
|
+
- ext/numaflow_ruby/Cargo.toml
|
47
|
+
extra_rdoc_files: []
|
48
|
+
files:
|
49
|
+
- ".rspec"
|
50
|
+
- ".rubocop.yml"
|
51
|
+
- CHANGELOG.md
|
52
|
+
- Cargo.lock
|
53
|
+
- Cargo.toml
|
54
|
+
- README.md
|
55
|
+
- Rakefile
|
56
|
+
- ext/numaflow_ruby/Cargo.toml
|
57
|
+
- ext/numaflow_ruby/extconf.rb
|
58
|
+
- ext/numaflow_ruby/src/lib.rs
|
59
|
+
- ext/numaflow_ruby/src/map.rs
|
60
|
+
- ext/numaflow_ruby/src/spec_helper.rs
|
61
|
+
- ext/numaflow_ruby/src/time.rs
|
62
|
+
- lib/numaflow_ruby.rb
|
63
|
+
- lib/numaflow_ruby/version.rb
|
64
|
+
- sig/numaflow_ruby.rbs
|
65
|
+
homepage: https://github.com/PlayerData/numaflow_ruby
|
66
|
+
licenses: []
|
67
|
+
metadata:
|
68
|
+
allowed_push_host: https://rubygems.org
|
69
|
+
homepage_uri: https://github.com/PlayerData/numaflow_ruby
|
70
|
+
source_code_uri: https://github.com/PlayerData/numaflow_ruby
|
71
|
+
changelog_uri: https://github.com/PlayerData/numaflow_ruby/tree/CHANGELOG.md
|
72
|
+
post_install_message:
|
73
|
+
rdoc_options: []
|
74
|
+
require_paths:
|
75
|
+
- lib
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: 3.0.0
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 3.3.11
|
86
|
+
requirements: []
|
87
|
+
rubygems_version: 3.5.22
|
88
|
+
signing_key:
|
89
|
+
specification_version: 4
|
90
|
+
summary: Ruby bindings for numaflow sdk
|
91
|
+
test_files: []
|