itsi 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.
- checksums.yaml +4 -4
- data/Cargo.lock +524 -44
- data/Rakefile +22 -33
- data/crates/itsi_error/Cargo.toml +2 -0
- data/crates/itsi_error/src/from.rs +70 -0
- data/crates/itsi_error/src/lib.rs +10 -37
- data/crates/itsi_instrument_entry/Cargo.toml +15 -0
- data/crates/itsi_instrument_entry/src/lib.rs +31 -0
- data/crates/itsi_rb_helpers/Cargo.toml +2 -0
- data/crates/itsi_rb_helpers/src/heap_value.rs +121 -0
- data/crates/itsi_rb_helpers/src/lib.rs +90 -10
- data/crates/itsi_scheduler/Cargo.toml +9 -1
- data/crates/itsi_scheduler/extconf.rb +1 -1
- data/crates/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +56 -0
- data/crates/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +44 -0
- data/crates/itsi_scheduler/src/itsi_scheduler/timer.rs +44 -0
- data/crates/itsi_scheduler/src/itsi_scheduler.rs +308 -0
- data/crates/itsi_scheduler/src/lib.rs +31 -10
- data/crates/itsi_server/Cargo.toml +14 -2
- data/crates/itsi_server/extconf.rb +1 -1
- data/crates/itsi_server/src/body_proxy/big_bytes.rs +104 -0
- data/crates/itsi_server/src/body_proxy/itsi_body_proxy.rs +122 -0
- data/crates/itsi_server/src/body_proxy/mod.rs +2 -0
- data/crates/itsi_server/src/lib.rs +58 -7
- data/crates/itsi_server/src/request/itsi_request.rs +238 -104
- data/crates/itsi_server/src/response/itsi_response.rs +347 -0
- data/crates/itsi_server/src/response/mod.rs +1 -0
- data/crates/itsi_server/src/server/bind.rs +50 -20
- data/crates/itsi_server/src/server/bind_protocol.rs +37 -0
- data/crates/itsi_server/src/server/io_stream.rs +104 -0
- data/crates/itsi_server/src/server/itsi_ca/itsi_ca.crt +11 -30
- data/crates/itsi_server/src/server/itsi_ca/itsi_ca.key +3 -50
- data/crates/itsi_server/src/server/itsi_server.rs +196 -134
- data/crates/itsi_server/src/server/lifecycle_event.rs +9 -0
- data/crates/itsi_server/src/server/listener.rs +184 -127
- data/crates/itsi_server/src/server/mod.rs +7 -1
- data/crates/itsi_server/src/server/process_worker.rs +196 -0
- data/crates/itsi_server/src/server/serve_strategy/cluster_mode.rs +254 -0
- data/crates/itsi_server/src/server/serve_strategy/mod.rs +27 -0
- data/crates/itsi_server/src/server/serve_strategy/single_mode.rs +241 -0
- data/crates/itsi_server/src/server/signal.rs +70 -0
- data/crates/itsi_server/src/server/thread_worker.rs +368 -0
- data/crates/itsi_server/src/server/tls.rs +42 -28
- data/crates/itsi_tracing/Cargo.toml +4 -0
- data/crates/itsi_tracing/src/lib.rs +36 -6
- data/gems/scheduler/Cargo.lock +219 -23
- data/gems/scheduler/Rakefile +7 -1
- data/gems/scheduler/ext/itsi_error/Cargo.toml +2 -0
- data/gems/scheduler/ext/itsi_error/src/from.rs +70 -0
- data/gems/scheduler/ext/itsi_error/src/lib.rs +10 -37
- data/gems/scheduler/ext/itsi_instrument_entry/Cargo.toml +15 -0
- data/gems/scheduler/ext/itsi_instrument_entry/src/lib.rs +31 -0
- data/gems/scheduler/ext/itsi_rb_helpers/Cargo.toml +2 -0
- data/gems/scheduler/ext/itsi_rb_helpers/src/heap_value.rs +121 -0
- data/gems/scheduler/ext/itsi_rb_helpers/src/lib.rs +90 -10
- data/gems/scheduler/ext/itsi_scheduler/Cargo.toml +9 -1
- data/gems/scheduler/ext/itsi_scheduler/extconf.rb +1 -1
- data/gems/scheduler/ext/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +56 -0
- data/gems/scheduler/ext/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +44 -0
- data/gems/scheduler/ext/itsi_scheduler/src/itsi_scheduler/timer.rs +44 -0
- data/gems/scheduler/ext/itsi_scheduler/src/itsi_scheduler.rs +308 -0
- data/gems/scheduler/ext/itsi_scheduler/src/lib.rs +31 -10
- data/gems/scheduler/ext/itsi_server/Cargo.toml +41 -0
- data/gems/scheduler/ext/itsi_server/extconf.rb +6 -0
- data/gems/scheduler/ext/itsi_server/src/body_proxy/big_bytes.rs +104 -0
- data/gems/scheduler/ext/itsi_server/src/body_proxy/itsi_body_proxy.rs +122 -0
- data/gems/scheduler/ext/itsi_server/src/body_proxy/mod.rs +2 -0
- data/gems/scheduler/ext/itsi_server/src/lib.rs +103 -0
- data/gems/scheduler/ext/itsi_server/src/request/itsi_request.rs +277 -0
- data/gems/scheduler/ext/itsi_server/src/request/mod.rs +1 -0
- data/gems/scheduler/ext/itsi_server/src/response/itsi_response.rs +347 -0
- data/gems/scheduler/ext/itsi_server/src/response/mod.rs +1 -0
- data/gems/scheduler/ext/itsi_server/src/server/bind.rs +168 -0
- data/gems/scheduler/ext/itsi_server/src/server/bind_protocol.rs +37 -0
- data/gems/scheduler/ext/itsi_server/src/server/io_stream.rs +104 -0
- data/gems/scheduler/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +13 -0
- data/gems/scheduler/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +5 -0
- data/gems/scheduler/ext/itsi_server/src/server/itsi_server.rs +244 -0
- data/gems/scheduler/ext/itsi_server/src/server/lifecycle_event.rs +9 -0
- data/gems/scheduler/ext/itsi_server/src/server/listener.rs +275 -0
- data/gems/scheduler/ext/itsi_server/src/server/mod.rs +11 -0
- data/gems/scheduler/ext/itsi_server/src/server/process_worker.rs +196 -0
- data/gems/scheduler/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +254 -0
- data/gems/scheduler/ext/itsi_server/src/server/serve_strategy/mod.rs +27 -0
- data/gems/scheduler/ext/itsi_server/src/server/serve_strategy/single_mode.rs +241 -0
- data/gems/scheduler/ext/itsi_server/src/server/signal.rs +70 -0
- data/gems/scheduler/ext/itsi_server/src/server/thread_worker.rs +368 -0
- data/gems/scheduler/ext/itsi_server/src/server/tls.rs +152 -0
- data/gems/scheduler/ext/itsi_tracing/Cargo.toml +4 -0
- data/gems/scheduler/ext/itsi_tracing/src/lib.rs +36 -6
- data/gems/scheduler/itsi-scheduler.gemspec +2 -3
- data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
- data/gems/scheduler/lib/itsi/scheduler.rb +137 -1
- data/gems/scheduler/test/helpers/test_helper.rb +24 -0
- data/gems/scheduler/test/test_active_record.rb +158 -0
- data/gems/scheduler/test/test_address_resolve.rb +23 -0
- data/gems/scheduler/test/test_block_unblock.rb +229 -0
- data/gems/scheduler/test/test_file_io.rb +193 -0
- data/gems/scheduler/test/test_itsi_scheduler.rb +24 -1
- data/gems/scheduler/test/test_kernel_sleep.rb +91 -0
- data/gems/scheduler/test/test_nested_fibers.rb +286 -0
- data/gems/scheduler/test/test_network_io.rb +274 -0
- data/gems/scheduler/test/test_process_wait.rb +26 -0
- data/gems/server/exe/itsi +88 -28
- data/gems/server/ext/itsi_error/Cargo.toml +2 -0
- data/gems/server/ext/itsi_error/src/from.rs +70 -0
- data/gems/server/ext/itsi_error/src/lib.rs +10 -37
- data/gems/server/ext/itsi_instrument_entry/Cargo.toml +15 -0
- data/gems/server/ext/itsi_instrument_entry/src/lib.rs +31 -0
- data/gems/server/ext/itsi_rb_helpers/Cargo.toml +2 -0
- data/gems/server/ext/itsi_rb_helpers/src/heap_value.rs +121 -0
- data/gems/server/ext/itsi_rb_helpers/src/lib.rs +90 -10
- data/gems/server/ext/itsi_scheduler/Cargo.toml +24 -0
- data/gems/server/ext/itsi_scheduler/extconf.rb +6 -0
- data/gems/server/ext/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +56 -0
- data/gems/server/ext/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +44 -0
- data/gems/server/ext/itsi_scheduler/src/itsi_scheduler/timer.rs +44 -0
- data/gems/server/ext/itsi_scheduler/src/itsi_scheduler.rs +308 -0
- data/gems/server/ext/itsi_scheduler/src/lib.rs +38 -0
- data/gems/server/ext/itsi_server/Cargo.toml +14 -2
- data/gems/server/ext/itsi_server/extconf.rb +1 -1
- data/gems/server/ext/itsi_server/src/body_proxy/big_bytes.rs +104 -0
- data/gems/server/ext/itsi_server/src/body_proxy/itsi_body_proxy.rs +122 -0
- data/gems/server/ext/itsi_server/src/body_proxy/mod.rs +2 -0
- data/gems/server/ext/itsi_server/src/lib.rs +58 -7
- data/gems/server/ext/itsi_server/src/request/itsi_request.rs +238 -104
- data/gems/server/ext/itsi_server/src/response/itsi_response.rs +347 -0
- data/gems/server/ext/itsi_server/src/response/mod.rs +1 -0
- data/gems/server/ext/itsi_server/src/server/bind.rs +50 -20
- data/gems/server/ext/itsi_server/src/server/bind_protocol.rs +37 -0
- data/gems/server/ext/itsi_server/src/server/io_stream.rs +104 -0
- data/gems/server/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +11 -30
- data/gems/server/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +3 -50
- data/gems/server/ext/itsi_server/src/server/itsi_server.rs +196 -134
- data/gems/server/ext/itsi_server/src/server/lifecycle_event.rs +9 -0
- data/gems/server/ext/itsi_server/src/server/listener.rs +184 -127
- data/gems/server/ext/itsi_server/src/server/mod.rs +7 -1
- data/gems/server/ext/itsi_server/src/server/process_worker.rs +196 -0
- data/gems/server/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +254 -0
- data/gems/server/ext/itsi_server/src/server/serve_strategy/mod.rs +27 -0
- data/gems/server/ext/itsi_server/src/server/serve_strategy/single_mode.rs +241 -0
- data/gems/server/ext/itsi_server/src/server/signal.rs +70 -0
- data/gems/server/ext/itsi_server/src/server/thread_worker.rs +368 -0
- data/gems/server/ext/itsi_server/src/server/tls.rs +42 -28
- data/gems/server/ext/itsi_tracing/Cargo.toml +4 -0
- data/gems/server/ext/itsi_tracing/src/lib.rs +36 -6
- data/gems/server/itsi-server.gemspec +4 -5
- data/gems/server/lib/itsi/request.rb +30 -14
- data/gems/server/lib/itsi/server/rack/handler/itsi.rb +25 -0
- data/gems/server/lib/itsi/server/scheduler_mode.rb +6 -0
- data/gems/server/lib/itsi/server/version.rb +1 -1
- data/gems/server/lib/itsi/server.rb +82 -2
- data/gems/server/lib/itsi/signals.rb +23 -0
- data/gems/server/lib/itsi/stream_io.rb +38 -0
- data/gems/server/test/test_helper.rb +2 -0
- data/gems/server/test/test_itsi_server.rb +1 -1
- data/lib/itsi/version.rb +1 -1
- data/tasks.txt +18 -0
- metadata +102 -12
- data/crates/itsi_server/src/server/transfer_protocol.rs +0 -23
- data/crates/itsi_server/src/stream_writer/mod.rs +0 -21
- data/gems/scheduler/test/test_helper.rb +0 -6
- data/gems/server/ext/itsi_server/src/server/transfer_protocol.rs +0 -23
- data/gems/server/ext/itsi_server/src/stream_writer/mod.rs +0 -21
@@ -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
|
+
}
|
@@ -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
|
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::
|
6
|
-
|
7
|
-
|
8
|
-
.
|
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
|
-
.
|
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
|
}
|
@@ -12,8 +12,8 @@ Gem::Specification.new do |spec|
|
|
12
12
|
spec.description = "Itsi Scheduler - A light-weight Fiber Scheduler implementation for Ruby"
|
13
13
|
spec.homepage = "https://itsi.fyi"
|
14
14
|
spec.license = "MIT"
|
15
|
-
spec.required_ruby_version = ">= 3.
|
16
|
-
spec.required_rubygems_version = ">= 3.
|
15
|
+
spec.required_ruby_version = ">= 3.0.0"
|
16
|
+
spec.required_rubygems_version = ">= 3.1.11"
|
17
17
|
|
18
18
|
spec.metadata["homepage_uri"] = spec.homepage
|
19
19
|
spec.metadata["source_code_uri"] = "https://github.com/wouterken/itsi/scheduler"
|
@@ -35,7 +35,6 @@ Gem::Specification.new do |spec|
|
|
35
35
|
|
36
36
|
# Uncomment to register a new dependency of your gem
|
37
37
|
# spec.add_dependency "example-gem", "~> 1.0"
|
38
|
-
spec.add_dependency "libclang", "~> 14.0"
|
39
38
|
spec.add_dependency "rb_sys", "~> 0.9.91"
|
40
39
|
|
41
40
|
# For more information and examples about making a new gem, check out our
|
@@ -6,6 +6,142 @@ require_relative "scheduler/itsi_scheduler"
|
|
6
6
|
module Itsi
|
7
7
|
class Scheduler
|
8
8
|
class Error < StandardError; end
|
9
|
-
|
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
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "minitest/reporters"
|
4
|
+
Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
|
5
|
+
|
6
|
+
require "itsi/scheduler"
|
7
|
+
require 'debug'
|
8
|
+
module Itsi::Scheduler::TestHelper
|
9
|
+
SchedulerClass = Itsi::Scheduler
|
10
|
+
|
11
|
+
def with_scheduler(join: true, report_on_exception: false)
|
12
|
+
Thread.new do
|
13
|
+
Thread.current.report_on_exception = report_on_exception
|
14
|
+
scheduler = SchedulerClass.new
|
15
|
+
Fiber.set_scheduler(scheduler)
|
16
|
+
Fiber.schedule do
|
17
|
+
yield scheduler
|
18
|
+
end
|
19
|
+
end.yield_self do |thread|
|
20
|
+
thread.join if join
|
21
|
+
thread
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record"
|
4
|
+
|
5
|
+
class TestActiveRecordFiberScheduler < Minitest::Test
|
6
|
+
include Itsi::Scheduler::TestHelper
|
7
|
+
|
8
|
+
# Set up an ActiveRecord connection to your PostgreSQL test database.
|
9
|
+
# Adjust the connection parameters as needed.
|
10
|
+
def setup
|
11
|
+
ActiveSupport::IsolatedExecutionState.isolation_level = :fiber
|
12
|
+
ActiveRecord::Base.establish_connection(
|
13
|
+
adapter: "postgresql",
|
14
|
+
database: "fiber_scheduler_test",
|
15
|
+
pool: 2, # use a small pool to test contention scenarios
|
16
|
+
checkout_timeout: 5
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Disconnect after each test.
|
21
|
+
def teardown
|
22
|
+
ActiveRecord::Base.connection_pool.disconnect!
|
23
|
+
end
|
24
|
+
|
25
|
+
# Test a basic query execution inside a fiber.
|
26
|
+
def test_basic_query
|
27
|
+
result = nil
|
28
|
+
|
29
|
+
with_scheduler do |_scheduler|
|
30
|
+
Fiber.schedule do
|
31
|
+
result = ActiveRecord::Base.connection.select_value("SELECT 1")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# select_value returns a string from PG adapter so we compare with "1"
|
36
|
+
assert_equal "1", result.to_s
|
37
|
+
end
|
38
|
+
|
39
|
+
# Test running two queries concurrently in different fibers.
|
40
|
+
def test_concurrent_queries
|
41
|
+
results = []
|
42
|
+
|
43
|
+
with_scheduler do |_scheduler|
|
44
|
+
Fiber.schedule do
|
45
|
+
results << ActiveRecord::Base.connection.select_value("SELECT 1")
|
46
|
+
end
|
47
|
+
|
48
|
+
Fiber.schedule do
|
49
|
+
results << ActiveRecord::Base.connection.select_value("SELECT 2")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Ensure that both queries have executed and returned the expected values.
|
54
|
+
results = results.map(&:to_s)
|
55
|
+
assert_includes results, "1"
|
56
|
+
assert_includes results, "2"
|
57
|
+
end
|
58
|
+
|
59
|
+
# Test a query that involves a short delay using PostgreSQL's pg_sleep.
|
60
|
+
def test_query_with_delay
|
61
|
+
result = nil
|
62
|
+
|
63
|
+
with_scheduler do |_scheduler|
|
64
|
+
Fiber.schedule do
|
65
|
+
# Introduce a 0.1 second delay.
|
66
|
+
ActiveRecord::Base.connection.execute("SELECT pg_sleep(0.1)")
|
67
|
+
result = ActiveRecord::Base.connection.select_value("SELECT 3")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
assert_equal "3", result.to_s
|
72
|
+
end
|
73
|
+
|
74
|
+
# Test connection pool exhaustion by limiting the pool to one connection.
|
75
|
+
# Two fibers will attempt to get a connection concurrently.
|
76
|
+
def test_connection_pool_exhaustion
|
77
|
+
# Re-establish connection with a pool size of 1.
|
78
|
+
ActiveSupport::IsolatedExecutionState.isolation_level = :fiber
|
79
|
+
ActiveRecord::Base.establish_connection(
|
80
|
+
adapter: "postgresql",
|
81
|
+
host: "localhost",
|
82
|
+
database: "fiber_scheduler_test",
|
83
|
+
pool: 1,
|
84
|
+
checkout_timeout: 0.25
|
85
|
+
)
|
86
|
+
# ActiveRecord::Base.connection_pool.disconnect!
|
87
|
+
|
88
|
+
results = []
|
89
|
+
|
90
|
+
with_scheduler do |_scheduler|
|
91
|
+
Fiber.schedule do
|
92
|
+
ActiveRecord::Base.connection_pool.with_connection(prevent_permanent_checkout: true) do
|
93
|
+
results << ActiveRecord::Base.connection.select_value("SELECT 1")
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
Fiber.schedule do
|
98
|
+
ActiveRecord::Base.connection_pool.with_connection(prevent_permanent_checkout: true) do
|
99
|
+
results << ActiveRecord::Base.connection.select_value("SELECT 2")
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Takes #{checkout_timeout} seconds between last
|
105
|
+
results = results.map(&:to_s)
|
106
|
+
assert_includes results, "1"
|
107
|
+
assert_includes results, "2"
|
108
|
+
end
|
109
|
+
|
110
|
+
# Test that after a Fiber finishes its work, its connection is automatically released.
|
111
|
+
def test_fiber_connection_release_after_completion
|
112
|
+
# Use the scheduler to run a fiber that checks out a connection and does a simple query.
|
113
|
+
|
114
|
+
with_scheduler do |_scheduler|
|
115
|
+
Fiber.schedule do
|
116
|
+
# This fiber checks out a connection to run a query.
|
117
|
+
ActiveRecord::Base.connection.select_value("SELECT 1")
|
118
|
+
# The fiber ends here.
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# After the scheduler finishes, the fiber should have completed and released its connection.
|
123
|
+
# Now we attempt to checkout a connection manually. If the previous fiber's connection
|
124
|
+
# was not released, this would either time out or raise an error.
|
125
|
+
connection = ActiveRecord::Base.connection_pool.checkout
|
126
|
+
assert connection, "Expected to obtain a connection after fiber completion"
|
127
|
+
ActiveRecord::Base.connection_pool.checkin(connection)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Test that a transaction works correctly when run inside a fiber.
|
131
|
+
# A temporary table is created, a record inserted and then queried before the transaction is rolled back.
|
132
|
+
def test_transaction_fiber
|
133
|
+
result = nil
|
134
|
+
|
135
|
+
with_scheduler do |_scheduler|
|
136
|
+
Fiber.schedule do
|
137
|
+
ActiveRecord::Base.transaction do
|
138
|
+
# Create a temporary table for testing.
|
139
|
+
ActiveRecord::Base.connection.execute(<<~SQL)
|
140
|
+
CREATE TEMP TABLE IF NOT EXISTS test_table (
|
141
|
+
id serial PRIMARY KEY,
|
142
|
+
name text
|
143
|
+
)
|
144
|
+
SQL
|
145
|
+
|
146
|
+
# Insert a record.
|
147
|
+
ActiveRecord::Base.connection.execute("INSERT INTO test_table (name) VALUES ('Alice')")
|
148
|
+
# Query the inserted record.
|
149
|
+
result = ActiveRecord::Base.connection.select_value("SELECT name FROM test_table LIMIT 1")
|
150
|
+
# Roll back the transaction to avoid leaving test data.
|
151
|
+
raise ActiveRecord::Rollback
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
assert_equal "Alice", result.to_s
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'debug'
|
3
|
+
|
4
|
+
|
5
|
+
class TestAddressResolve < Minitest::Test
|
6
|
+
include Itsi::Scheduler::TestHelper
|
7
|
+
|
8
|
+
def test_addess_resolve
|
9
|
+
results = []
|
10
|
+
|
11
|
+
with_scheduler do |_scheduler|
|
12
|
+
Fiber.schedule do
|
13
|
+
results << Addrinfo.getaddrinfo("www.ruby-lang.org", 80, nil, :STREAM)
|
14
|
+
end
|
15
|
+
Fiber.schedule do
|
16
|
+
results << Addrinfo.getaddrinfo("www.google.com", 80, nil, :STREAM)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
assert results.all?{|results| results.find(&:ipv4?) }
|
21
|
+
assert results.all?{|results| results.find(&:ipv6?) }
|
22
|
+
end
|
23
|
+
end
|