itsi-scheduler 0.1.0 → 0.1.3

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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +219 -23
  3. data/Rakefile +7 -1
  4. data/ext/itsi_error/Cargo.toml +2 -0
  5. data/ext/itsi_error/src/from.rs +70 -0
  6. data/ext/itsi_error/src/lib.rs +10 -37
  7. data/ext/itsi_instrument_entry/Cargo.toml +15 -0
  8. data/ext/itsi_instrument_entry/src/lib.rs +31 -0
  9. data/ext/itsi_rb_helpers/Cargo.toml +2 -0
  10. data/ext/itsi_rb_helpers/src/heap_value.rs +121 -0
  11. data/ext/itsi_rb_helpers/src/lib.rs +90 -10
  12. data/ext/itsi_scheduler/Cargo.toml +9 -1
  13. data/ext/itsi_scheduler/extconf.rb +1 -1
  14. data/ext/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +56 -0
  15. data/ext/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +44 -0
  16. data/ext/itsi_scheduler/src/itsi_scheduler/timer.rs +44 -0
  17. data/ext/itsi_scheduler/src/itsi_scheduler.rs +308 -0
  18. data/ext/itsi_scheduler/src/lib.rs +31 -10
  19. data/ext/itsi_server/Cargo.toml +41 -0
  20. data/ext/itsi_server/extconf.rb +6 -0
  21. data/ext/itsi_server/src/body_proxy/big_bytes.rs +104 -0
  22. data/ext/itsi_server/src/body_proxy/itsi_body_proxy.rs +122 -0
  23. data/ext/itsi_server/src/body_proxy/mod.rs +2 -0
  24. data/ext/itsi_server/src/lib.rs +103 -0
  25. data/ext/itsi_server/src/request/itsi_request.rs +277 -0
  26. data/ext/itsi_server/src/request/mod.rs +1 -0
  27. data/ext/itsi_server/src/response/itsi_response.rs +347 -0
  28. data/ext/itsi_server/src/response/mod.rs +1 -0
  29. data/ext/itsi_server/src/server/bind.rs +168 -0
  30. data/ext/itsi_server/src/server/bind_protocol.rs +37 -0
  31. data/ext/itsi_server/src/server/io_stream.rs +104 -0
  32. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +13 -0
  33. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +5 -0
  34. data/ext/itsi_server/src/server/itsi_server.rs +244 -0
  35. data/ext/itsi_server/src/server/lifecycle_event.rs +9 -0
  36. data/ext/itsi_server/src/server/listener.rs +275 -0
  37. data/ext/itsi_server/src/server/mod.rs +11 -0
  38. data/ext/itsi_server/src/server/process_worker.rs +196 -0
  39. data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +254 -0
  40. data/ext/itsi_server/src/server/serve_strategy/mod.rs +27 -0
  41. data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +241 -0
  42. data/ext/itsi_server/src/server/signal.rs +70 -0
  43. data/ext/itsi_server/src/server/thread_worker.rs +368 -0
  44. data/ext/itsi_server/src/server/tls.rs +152 -0
  45. data/ext/itsi_tracing/Cargo.toml +4 -0
  46. data/ext/itsi_tracing/src/lib.rs +36 -6
  47. data/lib/itsi/scheduler/version.rb +1 -1
  48. data/lib/itsi/scheduler.rb +137 -1
  49. metadata +38 -18
@@ -0,0 +1,152 @@
1
+ use base64::{engine::general_purpose, Engine as _};
2
+ use itsi_error::Result;
3
+ use itsi_tracing::{info, warn};
4
+ use rcgen::{CertificateParams, DnType, KeyPair, SanType};
5
+ use rustls_pemfile::{certs, pkcs8_private_keys};
6
+ use std::{collections::HashMap, fs, io::BufReader};
7
+ use tokio_rustls::rustls::{Certificate, PrivateKey, ServerConfig};
8
+
9
+ const ITS_CA_CERT: &str = include_str!("./itsi_ca/itsi_ca.crt");
10
+ const ITS_CA_KEY: &str = include_str!("./itsi_ca/itsi_ca.key");
11
+
12
+ // Generates a TLS configuration based on either :
13
+ // * Input "cert" and "key" options (either paths or Base64-encoded strings) or
14
+ // * Performs automatic certificate generation/retrieval. Generated certs use an internal self-signed Isti CA.
15
+ // If a non-local host or optional domain parameter is provided,
16
+ // an automated certificate will attempt to be fetched using let's encrypt.
17
+ pub fn configure_tls(host: &str, query_params: &HashMap<String, String>) -> Result<ServerConfig> {
18
+ info!("TLS Options {:?}", query_params);
19
+ let (certs, key) = if let (Some(cert_path), Some(key_path)) =
20
+ (query_params.get("cert"), query_params.get("key"))
21
+ {
22
+ // Load from file or Base64
23
+ let certs = load_certs(cert_path);
24
+ let key = load_private_key(key_path);
25
+ (certs, key)
26
+ } else {
27
+ let domains_param = query_params
28
+ .get("domains")
29
+ .map(|v| v.split(',').map(String::from).collect());
30
+ let host_string = host.to_string();
31
+ let domains = domains_param.or_else(|| {
32
+ if host_string != "localhost" {
33
+ Some(vec![host_string])
34
+ } else {
35
+ None
36
+ }
37
+ });
38
+
39
+ if let Some(domains) = domains {
40
+ retrieve_acme_cert(domains)?
41
+ } else {
42
+ generate_ca_signed_cert(vec![host.to_owned()])?
43
+ }
44
+ };
45
+
46
+ let mut config = ServerConfig::builder()
47
+ .with_safe_defaults()
48
+ .with_no_client_auth()
49
+ .with_single_cert(certs, key)
50
+ .expect("Failed to build TLS config");
51
+
52
+ config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
53
+ Ok(config)
54
+ }
55
+
56
+ pub fn load_certs(path: &str) -> Vec<Certificate> {
57
+ let data = if let Some(stripped) = path.strip_prefix("base64:") {
58
+ general_purpose::STANDARD
59
+ .decode(stripped)
60
+ .expect("Invalid base64 certificate")
61
+ } else {
62
+ fs::read(path).expect("Failed to read certificate file")
63
+ };
64
+
65
+ if data.starts_with(b"-----BEGIN ") {
66
+ let mut reader = BufReader::new(&data[..]);
67
+ let certs_der: Vec<Vec<u8>> = certs(&mut reader)
68
+ .map(|r| {
69
+ r.map(|der| der.as_ref().to_vec())
70
+ .map_err(itsi_error::ItsiError::from)
71
+ })
72
+ .collect::<Result<_>>()
73
+ .expect("Failed to parse certificate file");
74
+ certs_der.into_iter().map(Certificate).collect()
75
+ } else {
76
+ vec![Certificate(data)]
77
+ }
78
+ }
79
+
80
+ /// Loads a private key from a file or Base64.
81
+ pub fn load_private_key(path: &str) -> PrivateKey {
82
+ let key_data = if let Some(stripped) = path.strip_prefix("base64:") {
83
+ general_purpose::STANDARD
84
+ .decode(stripped)
85
+ .expect("Invalid base64 private key")
86
+ } else {
87
+ fs::read(path).expect("Failed to read private key file")
88
+ };
89
+
90
+ if key_data.starts_with(b"-----BEGIN ") {
91
+ let mut reader = BufReader::new(&key_data[..]);
92
+ let keys: Vec<Vec<u8>> = pkcs8_private_keys(&mut reader)
93
+ .map(|r| {
94
+ r.map(|key| key.secret_pkcs8_der().to_vec())
95
+ .map_err(itsi_error::ItsiError::from)
96
+ })
97
+ .collect::<Result<_>>()
98
+ .expect("Failed to parse private key");
99
+ if !keys.is_empty() {
100
+ return PrivateKey(keys[0].clone());
101
+ }
102
+ }
103
+ PrivateKey(key_data)
104
+ }
105
+
106
+ pub fn generate_ca_signed_cert(domains: Vec<String>) -> Result<(Vec<Certificate>, PrivateKey)> {
107
+ info!("Generating New Itsi CA - Self signed Certificate. Use `itsi ca export` to export the CA certificate for import into your local trust store.");
108
+
109
+ let ca_kp = KeyPair::from_pem(ITS_CA_KEY).expect("Failed to load embedded CA key");
110
+ let ca_cert = CertificateParams::from_ca_cert_pem(ITS_CA_CERT)
111
+ .expect("Failed to parse embedded CA certificate")
112
+ .self_signed(&ca_kp)
113
+ .expect("Failed to self-sign embedded CA cert");
114
+
115
+ let ee_key = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256).unwrap();
116
+ let mut ee_params = CertificateParams::default();
117
+
118
+ info!(
119
+ "Generated certificate will be valid for domains {:?}",
120
+ domains
121
+ );
122
+ use std::net::IpAddr;
123
+
124
+ ee_params.subject_alt_names = domains
125
+ .iter()
126
+ .map(|domain| {
127
+ if let Ok(ip) = domain.parse::<IpAddr>() {
128
+ SanType::IpAddress(ip)
129
+ } else {
130
+ SanType::DnsName(domain.clone().try_into().unwrap())
131
+ }
132
+ })
133
+ .collect();
134
+
135
+ ee_params
136
+ .distinguished_name
137
+ .push(DnType::CommonName, domains[0].clone());
138
+
139
+ ee_params.use_authority_key_identifier_extension = true;
140
+
141
+ let ee_cert = ee_params.signed_by(&ee_key, &ca_cert, &ca_kp).unwrap();
142
+ let ee_cert_der = ee_cert.der().to_vec();
143
+ let ee_cert = Certificate(ee_cert_der);
144
+ let ca_cert = Certificate(ca_cert.der().to_vec());
145
+ Ok((vec![ee_cert, ca_cert], PrivateKey(ee_key.serialize_der())))
146
+ }
147
+
148
+ /// TODO: Retrieves an ACME certificate for a given domain.
149
+ pub fn retrieve_acme_cert(domains: Vec<String>) -> Result<(Vec<Certificate>, PrivateKey)> {
150
+ warn!("Retrieving ACME cert for {}", domains.join(", "));
151
+ generate_ca_signed_cert(domains)
152
+ }
@@ -9,4 +9,8 @@ tracing-subscriber = { version = "0.3.19", features = [
9
9
  "env-filter",
10
10
  "std",
11
11
  "fmt",
12
+ "json",
13
+ "ansi",
12
14
  ] }
15
+ tracing-attributes = "0.1"
16
+ atty = "0.2.14"
@@ -1,11 +1,41 @@
1
+ use std::env;
2
+
3
+ use atty::{Stream, is};
1
4
  pub use tracing::{debug, error, info, trace, warn};
2
- use tracing_subscriber::{EnvFilter, fmt};
5
+ pub use tracing_attributes::instrument; // Explicitly export from tracing-attributes
6
+ use tracing_subscriber::{
7
+ EnvFilter,
8
+ fmt::{self, format},
9
+ };
3
10
 
11
+ #[instrument]
4
12
  pub fn init() {
5
- let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
6
- let format = fmt::format().with_level(true).with_target(false).compact();
7
- tracing_subscriber::fmt()
8
- .with_env_filter(env_filter)
13
+ let env_filter = EnvFilter::builder()
14
+ .with_env_var("ITSI_LOG")
15
+ .try_from_env()
16
+ .unwrap_or_else(|_| EnvFilter::new("info"));
17
+
18
+ let format = fmt::format()
19
+ .compact()
20
+ .with_file(false)
21
+ .with_level(true)
22
+ .with_line_number(false)
23
+ .with_source_location(false)
24
+ .with_target(false)
25
+ .with_thread_ids(false);
26
+
27
+ let is_tty = is(Stream::Stdout);
28
+
29
+ let subscriber = tracing_subscriber::fmt()
9
30
  .event_format(format)
10
- .init();
31
+ .with_env_filter(env_filter);
32
+
33
+ if (is_tty && env::var("ITSI_LOG_PLAIN").is_err()) || env::var("ITSI_LOG_ANSI").is_ok() {
34
+ subscriber.with_ansi(true).init();
35
+ } else {
36
+ subscriber
37
+ .fmt_fields(format::JsonFields::default())
38
+ .event_format(fmt::format().json())
39
+ .init();
40
+ }
11
41
  }
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Itsi
4
4
  class Scheduler
5
- VERSION = "0.1.0"
5
+ VERSION = "0.1.3"
6
6
  end
7
7
  end
@@ -6,6 +6,142 @@ require_relative "scheduler/itsi_scheduler"
6
6
  module Itsi
7
7
  class Scheduler
8
8
  class Error < StandardError; end
9
- # Your code goes here...
9
+
10
+ def self.resume_token
11
+ @resume_token ||= 0
12
+ @resume_token += 1
13
+ end
14
+
15
+ def initialize
16
+ @join_waiters = {}.compare_by_identity
17
+ @token_map = {}.compare_by_identity
18
+ @resume_tokens = {}.compare_by_identity
19
+ @unblocked = [[], []]
20
+ @unblock_idx = 0
21
+ @unblocked_mux = Mutex.new
22
+ @resume_fiber = method(:resume_fiber).to_proc
23
+ @resume_fiber_with_readiness = method(:resume_fiber_with_readiness).to_proc
24
+ @resume_blocked = method(:resume_blocked).to_proc
25
+ end
26
+
27
+ def block(_, timeout, fiber = Fiber.current, token = Scheduler.resume_token)
28
+ @join_waiters[fiber] = true
29
+
30
+ start_timer(timeout, token) if timeout
31
+ @resume_tokens[token] = fiber
32
+ @token_map[fiber] = token
33
+ Fiber.yield
34
+ ensure
35
+ @resume_tokens.delete(token)
36
+ @token_map.delete(fiber)
37
+ @join_waiters.delete(fiber)
38
+ end
39
+
40
+ # Register an IO waiter.
41
+ # This will get resumed by our scheduler inside the call to
42
+ # fetch_events.
43
+ def io_wait(io, events, duration)
44
+ fiber = Fiber.current
45
+ token = Scheduler.resume_token
46
+ readiness = register_io_wait(io.fileno, events, duration, token)
47
+ readiness || block(nil, duration, fiber, token)
48
+ end
49
+
50
+ def unblock(_blocker, fiber)
51
+ @unblocked_mux.synchronize do
52
+ @unblocked[@unblock_idx] << fiber
53
+ end
54
+ wake
55
+ end
56
+
57
+ def kernel_sleep(duration)
58
+ block nil, duration
59
+ end
60
+
61
+ def tick
62
+ events = fetch_due_events
63
+ timers = fetch_due_timers
64
+ unblocked = switch_unblock_batch
65
+ events&.each(&@resume_fiber_with_readiness)
66
+ unblocked.each(&@resume_blocked)
67
+ unblocked.clear
68
+ timers&.each(&@resume_fiber)
69
+ end
70
+
71
+ def resume_fiber(token)
72
+ if (fiber = @resume_tokens.delete(token))
73
+ fiber.resume
74
+ end
75
+ rescue StandardError => e
76
+ warn "Failed to resume fiber #{fiber}: #{e.message}"
77
+ end
78
+
79
+ def resume_fiber_with_readiness((token, readiness))
80
+ if (fiber = @resume_tokens.delete(token))
81
+ fiber.resume(readiness)
82
+ end
83
+ rescue StandardError => e
84
+ warn "Failed to resume fiber #{fiber}: #{e.message}"
85
+ end
86
+
87
+ def resume_blocked(fiber)
88
+ if (token = @token_map[fiber])
89
+ resume_fiber(token)
90
+ elsif fiber.alive?
91
+ fiber.resume
92
+ end
93
+ end
94
+
95
+ def switch_unblock_batch
96
+ @unblocked_mux.synchronize do
97
+ current = @unblocked[@unblock_idx]
98
+ @unblock_idx = (@unblock_idx + 1) % 2
99
+ current
100
+ end
101
+ end
102
+
103
+ # Yields upwards to the scheduler, with an intention to
104
+ # resume the fiber that yielded ASAP.
105
+ def yield
106
+ kernel_sleep(0) if work?
107
+ end
108
+
109
+ # Keep running until we've got no timers we're awaiting, no pending IO, no temporary yields,
110
+ # no pending unblocks.
111
+ def work?
112
+ !@unblocked[@unblock_idx].empty? || !@join_waiters.empty? || has_pending_io?
113
+ end
114
+
115
+ # Run until no more work needs doing.
116
+ def run
117
+ tick while work?
118
+ debug "Exit Scheduler"
119
+ end
120
+
121
+ # Hook invoked at the end of the thread.
122
+ # Will start our scheduler's Reactor.
123
+ def scheduler_close
124
+ run
125
+ ensure
126
+ @closed ||= true
127
+ freeze
128
+ end
129
+
130
+ # Need to defer to Process::Status rather than our extension
131
+ # as we don't have a means of creating our own Process::Status.
132
+ def process_wait(pid, flags)
133
+ Thread.new do
134
+ Process::Status.wait(pid, flags)
135
+ end.value
136
+ end
137
+
138
+ def closed?
139
+ @closed
140
+ end
141
+
142
+ # Spin up a new fiber and immediately resume it.
143
+ def fiber(&blk)
144
+ Fiber.new(blocking: false, &blk).tap(&:resume)
145
+ end
10
146
  end
11
147
  end
metadata CHANGED
@@ -1,28 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: itsi-scheduler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wouter Coppieters
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-03-01 00:00:00.000000000 Z
10
+ date: 2025-03-14 00:00:00.000000000 Z
11
11
  dependencies:
12
- - !ruby/object:Gem::Dependency
13
- name: libclang
14
- requirement: !ruby/object:Gem::Requirement
15
- requirements:
16
- - - "~>"
17
- - !ruby/object:Gem::Version
18
- version: '14.0'
19
- type: :runtime
20
- prerelease: false
21
- version_requirements: !ruby/object:Gem::Requirement
22
- requirements:
23
- - - "~>"
24
- - !ruby/object:Gem::Version
25
- version: '14.0'
26
12
  - !ruby/object:Gem::Dependency
27
13
  name: rb_sys
28
14
  requirement: !ruby/object:Gem::Requirement
@@ -56,13 +42,47 @@ files:
56
42
  - Rakefile
57
43
  - ext/itsi_error/Cargo.lock
58
44
  - ext/itsi_error/Cargo.toml
45
+ - ext/itsi_error/src/from.rs
59
46
  - ext/itsi_error/src/lib.rs
47
+ - ext/itsi_instrument_entry/Cargo.toml
48
+ - ext/itsi_instrument_entry/src/lib.rs
60
49
  - ext/itsi_rb_helpers/Cargo.lock
61
50
  - ext/itsi_rb_helpers/Cargo.toml
51
+ - ext/itsi_rb_helpers/src/heap_value.rs
62
52
  - ext/itsi_rb_helpers/src/lib.rs
63
53
  - ext/itsi_scheduler/Cargo.toml
64
54
  - ext/itsi_scheduler/extconf.rb
55
+ - ext/itsi_scheduler/src/itsi_scheduler.rs
56
+ - ext/itsi_scheduler/src/itsi_scheduler/io_helpers.rs
57
+ - ext/itsi_scheduler/src/itsi_scheduler/io_waiter.rs
58
+ - ext/itsi_scheduler/src/itsi_scheduler/timer.rs
65
59
  - ext/itsi_scheduler/src/lib.rs
60
+ - ext/itsi_server/Cargo.toml
61
+ - ext/itsi_server/extconf.rb
62
+ - ext/itsi_server/src/body_proxy/big_bytes.rs
63
+ - ext/itsi_server/src/body_proxy/itsi_body_proxy.rs
64
+ - ext/itsi_server/src/body_proxy/mod.rs
65
+ - ext/itsi_server/src/lib.rs
66
+ - ext/itsi_server/src/request/itsi_request.rs
67
+ - ext/itsi_server/src/request/mod.rs
68
+ - ext/itsi_server/src/response/itsi_response.rs
69
+ - ext/itsi_server/src/response/mod.rs
70
+ - ext/itsi_server/src/server/bind.rs
71
+ - ext/itsi_server/src/server/bind_protocol.rs
72
+ - ext/itsi_server/src/server/io_stream.rs
73
+ - ext/itsi_server/src/server/itsi_ca/itsi_ca.crt
74
+ - ext/itsi_server/src/server/itsi_ca/itsi_ca.key
75
+ - ext/itsi_server/src/server/itsi_server.rs
76
+ - ext/itsi_server/src/server/lifecycle_event.rs
77
+ - ext/itsi_server/src/server/listener.rs
78
+ - ext/itsi_server/src/server/mod.rs
79
+ - ext/itsi_server/src/server/process_worker.rs
80
+ - ext/itsi_server/src/server/serve_strategy/cluster_mode.rs
81
+ - ext/itsi_server/src/server/serve_strategy/mod.rs
82
+ - ext/itsi_server/src/server/serve_strategy/single_mode.rs
83
+ - ext/itsi_server/src/server/signal.rs
84
+ - ext/itsi_server/src/server/thread_worker.rs
85
+ - ext/itsi_server/src/server/tls.rs
66
86
  - ext/itsi_tracing/Cargo.lock
67
87
  - ext/itsi_tracing/Cargo.toml
68
88
  - ext/itsi_tracing/src/lib.rs
@@ -83,12 +103,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
83
103
  requirements:
84
104
  - - ">="
85
105
  - !ruby/object:Gem::Version
86
- version: 3.1.0
106
+ version: 3.0.0
87
107
  required_rubygems_version: !ruby/object:Gem::Requirement
88
108
  requirements:
89
109
  - - ">="
90
110
  - !ruby/object:Gem::Version
91
- version: 3.3.11
111
+ version: 3.1.11
92
112
  requirements: []
93
113
  rubygems_version: 3.6.2
94
114
  specification_version: 4