datadog 2.7.0 → 2.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +43 -1
- data/ext/datadog_profiling_native_extension/collectors_thread_context.c +47 -17
- data/ext/datadog_profiling_native_extension/extconf.rb +3 -8
- data/ext/datadog_profiling_native_extension/heap_recorder.c +11 -89
- data/ext/datadog_profiling_native_extension/private_vm_api_access.c +3 -9
- data/ext/datadog_profiling_native_extension/profiling.c +6 -0
- data/ext/datadog_profiling_native_extension/ruby_helpers.c +14 -4
- data/ext/datadog_profiling_native_extension/ruby_helpers.h +4 -0
- data/ext/datadog_profiling_native_extension/stack_recorder.c +0 -34
- data/ext/libdatadog_extconf_helpers.rb +1 -1
- data/lib/datadog/appsec/component.rb +1 -8
- data/lib/datadog/appsec/contrib/active_record/instrumentation.rb +73 -0
- data/lib/datadog/appsec/contrib/active_record/integration.rb +41 -0
- data/lib/datadog/appsec/contrib/active_record/patcher.rb +53 -0
- data/lib/datadog/appsec/event.rb +1 -1
- data/lib/datadog/appsec/processor/context.rb +2 -2
- data/lib/datadog/appsec/remote.rb +1 -3
- data/lib/datadog/appsec/response.rb +7 -11
- data/lib/datadog/appsec.rb +3 -2
- data/lib/datadog/core/configuration/components.rb +17 -1
- data/lib/datadog/core/configuration/settings.rb +10 -0
- data/lib/datadog/core/configuration.rb +9 -1
- data/lib/datadog/core/remote/client/capabilities.rb +6 -0
- data/lib/datadog/core/remote/client.rb +65 -59
- data/lib/datadog/core/telemetry/component.rb +9 -3
- data/lib/datadog/core/telemetry/ext.rb +1 -0
- data/lib/datadog/di/code_tracker.rb +5 -4
- data/lib/datadog/di/component.rb +5 -1
- data/lib/datadog/di/contrib/active_record.rb +1 -0
- data/lib/datadog/di/init.rb +20 -0
- data/lib/datadog/di/instrumenter.rb +81 -11
- data/lib/datadog/di/probe.rb +11 -1
- data/lib/datadog/di/probe_builder.rb +1 -0
- data/lib/datadog/di/probe_manager.rb +4 -1
- data/lib/datadog/di/probe_notification_builder.rb +13 -7
- data/lib/datadog/di/remote.rb +124 -0
- data/lib/datadog/di/serializer.rb +14 -7
- data/lib/datadog/di/transport.rb +2 -1
- data/lib/datadog/di/utils.rb +7 -0
- data/lib/datadog/di.rb +84 -20
- data/lib/datadog/profiling/component.rb +4 -16
- data/lib/datadog/tracing/configuration/settings.rb +4 -8
- data/lib/datadog/tracing/contrib/active_support/cache/redis.rb +16 -4
- data/lib/datadog/tracing/contrib/elasticsearch/configuration/settings.rb +4 -0
- data/lib/datadog/tracing/contrib/elasticsearch/patcher.rb +6 -1
- data/lib/datadog/tracing/tracer.rb +1 -1
- data/lib/datadog/version.rb +1 -1
- data/lib/datadog.rb +3 -0
- metadata +17 -13
- data/lib/datadog/appsec/processor/actions.rb +0 -49
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6f4b811f0c5014e6f325ac55406d225454a2101cc7576df15657ebc74cf47542
|
4
|
+
data.tar.gz: 20c6095b149238c31501bd0be8eb4363a9b22011f750d5f2d68e3f8b730bb970
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: af600463b83509c10417cc90fa808b4148baaa0961bde7aa2d1cdea98c6537a68fdde456af2d700a1e8d795a49fbc9aef3f61d68cc81949132abd8d9165ed19e
|
7
|
+
data.tar.gz: 28977b792b9f957e57bf8386b759a59257d0a74c161038ae7f2f4a1b0f8016b8c3828cf71092920c6fd14fd6640e5ab4dbf987dec257fc2ff54061f7342c20bf
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,35 @@
|
|
2
2
|
|
3
3
|
## [Unreleased]
|
4
4
|
|
5
|
+
## [2.8.0] - 2024-12-10
|
6
|
+
|
7
|
+
### Added
|
8
|
+
|
9
|
+
* DI: Dynamic instrumentation is now available in Ruby as a Preview
|
10
|
+
* AppSec: Add SQL injection detection for ActiveRecord for following adapters: `mysql2`, `postgresql`, and `sqlite3` ([#4167][])
|
11
|
+
* Telemetry: Add environment variable to disable logs ([#4153][])
|
12
|
+
* Integrations: Add configuration option `on_error` to Elasticsearch tracing ([#4066][])
|
13
|
+
|
14
|
+
### Changed
|
15
|
+
|
16
|
+
* Upgrade libdatadog dependency to 14.3.1 ([#4196][])
|
17
|
+
* Profiling: Require Ruby 3.1+ for heap profiling ([#4178][])
|
18
|
+
* AppSec: Update libddwaf to 1.18.0.0.0 ([#4164][])
|
19
|
+
* Single-step: Lower SSI GLIBC requirements down to 2.17 ([#4137][])
|
20
|
+
|
21
|
+
### Fixed
|
22
|
+
|
23
|
+
* Integrations: Avoid loading `ActiveSupport::Cache::RedisCacheStore`, which tries to load `redis >= 4.0.1` regardless of the version of Redis the host application has installed ([#4197][])
|
24
|
+
* Profiling: Fix unsafe initialization when using profiler with otel tracing ([#4195][])
|
25
|
+
* Single-step: Add safe NOOP injection script for very old rubies ([#4140][])
|
26
|
+
|
27
|
+
## [2.7.1] - 2024-11-28
|
28
|
+
|
29
|
+
### Fixed
|
30
|
+
|
31
|
+
* Tracing: Fix missing version tag ([#4075][])
|
32
|
+
* Profiling: Fix profiling not loading in certain situations on Ruby 2.5 and 3.3 ([#4161][])
|
33
|
+
|
5
34
|
## [2.7.0] - 2024-11-13
|
6
35
|
|
7
36
|
### Added
|
@@ -3028,7 +3057,8 @@ Release notes: https://github.com/DataDog/dd-trace-rb/releases/tag/v0.3.1
|
|
3028
3057
|
Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
|
3029
3058
|
|
3030
3059
|
|
3031
|
-
[Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v2.
|
3060
|
+
[Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v2.8.0...master
|
3061
|
+
[2.8.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.7.1...v2.8.0
|
3032
3062
|
[2.7.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.6.0...v2.7.0
|
3033
3063
|
[2.6.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.5.0...v2.6.0
|
3034
3064
|
[2.5.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.4.0...v2.5.0
|
@@ -4473,10 +4503,22 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
|
|
4473
4503
|
[#4027]: https://github.com/DataDog/dd-trace-rb/issues/4027
|
4474
4504
|
[#4033]: https://github.com/DataDog/dd-trace-rb/issues/4033
|
4475
4505
|
[#4065]: https://github.com/DataDog/dd-trace-rb/issues/4065
|
4506
|
+
[#4066]: https://github.com/DataDog/dd-trace-rb/issues/4066
|
4507
|
+
[#4075]: https://github.com/DataDog/dd-trace-rb/issues/4075
|
4476
4508
|
[#4078]: https://github.com/DataDog/dd-trace-rb/issues/4078
|
4477
4509
|
[#4082]: https://github.com/DataDog/dd-trace-rb/issues/4082
|
4478
4510
|
[#4083]: https://github.com/DataDog/dd-trace-rb/issues/4083
|
4479
4511
|
[#4085]: https://github.com/DataDog/dd-trace-rb/issues/4085
|
4512
|
+
[#4137]: https://github.com/DataDog/dd-trace-rb/issues/4137
|
4513
|
+
[#4140]: https://github.com/DataDog/dd-trace-rb/issues/4140
|
4514
|
+
[#4153]: https://github.com/DataDog/dd-trace-rb/issues/4153
|
4515
|
+
[#4161]: https://github.com/DataDog/dd-trace-rb/issues/4161
|
4516
|
+
[#4164]: https://github.com/DataDog/dd-trace-rb/issues/4164
|
4517
|
+
[#4167]: https://github.com/DataDog/dd-trace-rb/issues/4167
|
4518
|
+
[#4178]: https://github.com/DataDog/dd-trace-rb/issues/4178
|
4519
|
+
[#4195]: https://github.com/DataDog/dd-trace-rb/issues/4195
|
4520
|
+
[#4196]: https://github.com/DataDog/dd-trace-rb/issues/4196
|
4521
|
+
[#4197]: https://github.com/DataDog/dd-trace-rb/issues/4197
|
4480
4522
|
[@AdrianLC]: https://github.com/AdrianLC
|
4481
4523
|
[@Azure7111]: https://github.com/Azure7111
|
4482
4524
|
[@BabyGroot]: https://github.com/BabyGroot
|
@@ -226,7 +226,8 @@ static void trigger_sample_for_thread(
|
|
226
226
|
long current_monotonic_wall_time_ns,
|
227
227
|
ddog_CharSlice *ruby_vm_type,
|
228
228
|
ddog_CharSlice *class_name,
|
229
|
-
bool is_gvl_waiting_state
|
229
|
+
bool is_gvl_waiting_state,
|
230
|
+
bool is_safe_to_allocate_objects
|
230
231
|
);
|
231
232
|
static VALUE _native_thread_list(VALUE self);
|
232
233
|
static struct per_thread_context *get_or_create_context_for(VALUE thread, struct thread_context_collector_state *state);
|
@@ -246,7 +247,12 @@ static long cpu_time_now_ns(struct per_thread_context *thread_context);
|
|
246
247
|
static long thread_id_for(VALUE thread);
|
247
248
|
static VALUE _native_stats(VALUE self, VALUE collector_instance);
|
248
249
|
static VALUE _native_gc_tracking(VALUE self, VALUE collector_instance);
|
249
|
-
static void trace_identifiers_for(
|
250
|
+
static void trace_identifiers_for(
|
251
|
+
struct thread_context_collector_state *state,
|
252
|
+
VALUE thread,
|
253
|
+
struct trace_identifiers *trace_identifiers_result,
|
254
|
+
bool is_safe_to_allocate_objects
|
255
|
+
);
|
250
256
|
static bool should_collect_resource(VALUE root_span);
|
251
257
|
static VALUE _native_reset_after_fork(DDTRACE_UNUSED VALUE self, VALUE collector_instance);
|
252
258
|
static VALUE thread_list(struct thread_context_collector_state *state);
|
@@ -259,7 +265,8 @@ static void ddtrace_otel_trace_identifiers_for(
|
|
259
265
|
VALUE *root_span,
|
260
266
|
VALUE *numeric_span_id,
|
261
267
|
VALUE active_span,
|
262
|
-
VALUE otel_values
|
268
|
+
VALUE otel_values,
|
269
|
+
bool is_safe_to_allocate_objects
|
263
270
|
);
|
264
271
|
static VALUE _native_sample_skipped_allocation_samples(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE skipped_samples);
|
265
272
|
static bool handle_gvl_waiting(
|
@@ -278,7 +285,8 @@ static VALUE _native_apply_delta_to_cpu_time_at_previous_sample_ns(DDTRACE_UNUSE
|
|
278
285
|
static void otel_without_ddtrace_trace_identifiers_for(
|
279
286
|
struct thread_context_collector_state *state,
|
280
287
|
VALUE thread,
|
281
|
-
struct trace_identifiers *trace_identifiers_result
|
288
|
+
struct trace_identifiers *trace_identifiers_result,
|
289
|
+
bool is_safe_to_allocate_objects
|
282
290
|
);
|
283
291
|
static struct otel_span otel_span_from(VALUE otel_context, VALUE otel_current_span_key);
|
284
292
|
static uint64_t otel_span_id_to_uint(VALUE otel_span_id);
|
@@ -647,7 +655,8 @@ static void update_metrics_and_sample(
|
|
647
655
|
current_monotonic_wall_time_ns,
|
648
656
|
NULL,
|
649
657
|
NULL,
|
650
|
-
is_gvl_waiting_state
|
658
|
+
is_gvl_waiting_state,
|
659
|
+
/* is_safe_to_allocate_objects: */ true // We called from a context that's safe to run any regular code, including allocations
|
651
660
|
);
|
652
661
|
}
|
653
662
|
|
@@ -833,7 +842,10 @@ static void trigger_sample_for_thread(
|
|
833
842
|
// These two labels are only used for allocation profiling; @ivoanjo: may want to refactor this at some point?
|
834
843
|
ddog_CharSlice *ruby_vm_type,
|
835
844
|
ddog_CharSlice *class_name,
|
836
|
-
bool is_gvl_waiting_state
|
845
|
+
bool is_gvl_waiting_state,
|
846
|
+
// If the Ruby VM is at a state that can allocate objects safely, or not. Added for allocation profiling: we're not
|
847
|
+
// allowed to allocate objects (or raise exceptions) when inside the NEWOBJ tracepoint.
|
848
|
+
bool is_safe_to_allocate_objects
|
837
849
|
) {
|
838
850
|
int max_label_count =
|
839
851
|
1 + // thread id
|
@@ -872,11 +884,11 @@ static void trigger_sample_for_thread(
|
|
872
884
|
}
|
873
885
|
|
874
886
|
struct trace_identifiers trace_identifiers_result = {.valid = false, .trace_endpoint = Qnil};
|
875
|
-
trace_identifiers_for(state, thread, &trace_identifiers_result);
|
887
|
+
trace_identifiers_for(state, thread, &trace_identifiers_result, is_safe_to_allocate_objects);
|
876
888
|
|
877
889
|
if (!trace_identifiers_result.valid && state->otel_context_enabled != OTEL_CONTEXT_ENABLED_FALSE) {
|
878
890
|
// If we couldn't get something with ddtrace, let's see if we can get some trace identifiers from opentelemetry directly
|
879
|
-
otel_without_ddtrace_trace_identifiers_for(state, thread, &trace_identifiers_result);
|
891
|
+
otel_without_ddtrace_trace_identifiers_for(state, thread, &trace_identifiers_result, is_safe_to_allocate_objects);
|
880
892
|
}
|
881
893
|
|
882
894
|
if (trace_identifiers_result.valid) {
|
@@ -1289,7 +1301,12 @@ static VALUE _native_gc_tracking(DDTRACE_UNUSED VALUE _self, VALUE collector_ins
|
|
1289
1301
|
}
|
1290
1302
|
|
1291
1303
|
// Assumption 1: This function is called in a thread that is holding the Global VM Lock. Caller is responsible for enforcing this.
|
1292
|
-
static void trace_identifiers_for(
|
1304
|
+
static void trace_identifiers_for(
|
1305
|
+
struct thread_context_collector_state *state,
|
1306
|
+
VALUE thread,
|
1307
|
+
struct trace_identifiers *trace_identifiers_result,
|
1308
|
+
bool is_safe_to_allocate_objects
|
1309
|
+
) {
|
1293
1310
|
if (state->otel_context_enabled == OTEL_CONTEXT_ENABLED_ONLY) return;
|
1294
1311
|
if (state->tracer_context_key == MISSING_TRACER_CONTEXT_KEY) return;
|
1295
1312
|
|
@@ -1308,7 +1325,9 @@ static void trace_identifiers_for(struct thread_context_collector_state *state,
|
|
1308
1325
|
|
1309
1326
|
VALUE numeric_span_id = Qnil;
|
1310
1327
|
|
1311
|
-
if (otel_values != Qnil)
|
1328
|
+
if (otel_values != Qnil) {
|
1329
|
+
ddtrace_otel_trace_identifiers_for(state, &active_trace, &root_span, &numeric_span_id, active_span, otel_values, is_safe_to_allocate_objects);
|
1330
|
+
}
|
1312
1331
|
|
1313
1332
|
if (root_span == Qnil || (active_span == Qnil && numeric_span_id == Qnil)) return;
|
1314
1333
|
|
@@ -1474,7 +1493,8 @@ void thread_context_collector_sample_allocation(VALUE self_instance, unsigned in
|
|
1474
1493
|
INVALID_TIME, // For now we're not collecting timestamps for allocation events, as per profiling team internal discussions
|
1475
1494
|
&ruby_vm_type,
|
1476
1495
|
optional_class_name,
|
1477
|
-
false
|
1496
|
+
/* is_gvl_waiting_state: */ false,
|
1497
|
+
/* is_safe_to_allocate_objects: */ false // Not safe to allocate further inside the NEWOBJ tracepoint
|
1478
1498
|
);
|
1479
1499
|
}
|
1480
1500
|
|
@@ -1529,11 +1549,18 @@ static VALUE read_otel_current_span_key_const(DDTRACE_UNUSED VALUE _unused) {
|
|
1529
1549
|
return rb_const_get(trace_module, rb_intern("CURRENT_SPAN_KEY"));
|
1530
1550
|
}
|
1531
1551
|
|
1532
|
-
static VALUE get_otel_current_span_key(struct thread_context_collector_state *state) {
|
1552
|
+
static VALUE get_otel_current_span_key(struct thread_context_collector_state *state, bool is_safe_to_allocate_objects) {
|
1533
1553
|
if (state->otel_current_span_key == Qtrue) { // Qtrue means we haven't tried to extract it yet
|
1554
|
+
if (!is_safe_to_allocate_objects) {
|
1555
|
+
// Calling read_otel_current_span_key_const below can trigger exceptions and arbitrary Ruby code running (e.g.
|
1556
|
+
// `const_missing`, etc). Not safe to call in this situation, so we just skip otel info for this sample.
|
1557
|
+
return Qnil;
|
1558
|
+
}
|
1559
|
+
|
1534
1560
|
// If this fails, we want to fail gracefully, rather than raise an exception (e.g. if the opentelemetry gem
|
1535
1561
|
// gets refactored, we should not fall on our face)
|
1536
1562
|
VALUE span_key = rb_protect(read_otel_current_span_key_const, Qnil, NULL);
|
1563
|
+
rb_set_errinfo(Qnil); // **Clear any pending exception after ignoring it**
|
1537
1564
|
|
1538
1565
|
// Note that this gets set to Qnil if we failed to extract the correct value, and thus we won't try to extract it again
|
1539
1566
|
state->otel_current_span_key = span_key;
|
@@ -1550,7 +1577,8 @@ static void ddtrace_otel_trace_identifiers_for(
|
|
1550
1577
|
VALUE *root_span,
|
1551
1578
|
VALUE *numeric_span_id,
|
1552
1579
|
VALUE active_span,
|
1553
|
-
VALUE otel_values
|
1580
|
+
VALUE otel_values,
|
1581
|
+
bool is_safe_to_allocate_objects
|
1554
1582
|
) {
|
1555
1583
|
VALUE resolved_numeric_span_id =
|
1556
1584
|
active_span == Qnil ?
|
@@ -1561,7 +1589,7 @@ static void ddtrace_otel_trace_identifiers_for(
|
|
1561
1589
|
|
1562
1590
|
if (resolved_numeric_span_id == Qnil) return;
|
1563
1591
|
|
1564
|
-
VALUE otel_current_span_key = get_otel_current_span_key(state);
|
1592
|
+
VALUE otel_current_span_key = get_otel_current_span_key(state, is_safe_to_allocate_objects);
|
1565
1593
|
if (otel_current_span_key == Qnil) return;
|
1566
1594
|
VALUE current_trace = *active_trace;
|
1567
1595
|
|
@@ -1640,14 +1668,15 @@ static VALUE _native_sample_skipped_allocation_samples(DDTRACE_UNUSED VALUE self
|
|
1640
1668
|
static void otel_without_ddtrace_trace_identifiers_for(
|
1641
1669
|
struct thread_context_collector_state *state,
|
1642
1670
|
VALUE thread,
|
1643
|
-
struct trace_identifiers *trace_identifiers_result
|
1671
|
+
struct trace_identifiers *trace_identifiers_result,
|
1672
|
+
bool is_safe_to_allocate_objects
|
1644
1673
|
) {
|
1645
1674
|
VALUE context_storage = rb_thread_local_aref(thread, otel_context_storage_id /* __opentelemetry_context_storage__ */);
|
1646
1675
|
|
1647
1676
|
// If it exists, context_storage is expected to be an Array[OpenTelemetry::Context]
|
1648
1677
|
if (context_storage == Qnil || !RB_TYPE_P(context_storage, T_ARRAY)) return;
|
1649
1678
|
|
1650
|
-
VALUE otel_current_span_key = get_otel_current_span_key(state);
|
1679
|
+
VALUE otel_current_span_key = get_otel_current_span_key(state, is_safe_to_allocate_objects);
|
1651
1680
|
if (otel_current_span_key == Qnil) return;
|
1652
1681
|
|
1653
1682
|
int active_context_index = RARRAY_LEN(context_storage) - 1;
|
@@ -1939,7 +1968,8 @@ static uint64_t otel_span_id_to_uint(VALUE otel_span_id) {
|
|
1939
1968
|
gvl_waiting_started_wall_time_ns,
|
1940
1969
|
NULL,
|
1941
1970
|
NULL,
|
1942
|
-
false // This is the extra sample before the wait begun; only the next sample will be in the gvl waiting state
|
1971
|
+
/* is_gvl_waiting_state: */ false, // This is the extra sample before the wait begun; only the next sample will be in the gvl waiting state
|
1972
|
+
/* is_safe_to_allocate_objects: */ true // This is similar to a regular cpu/wall sample, so it's also safe
|
1943
1973
|
);
|
1944
1974
|
}
|
1945
1975
|
|
@@ -131,6 +131,9 @@ end
|
|
131
131
|
|
132
132
|
have_func "malloc_stats"
|
133
133
|
|
134
|
+
# On Ruby 2.5 and 3.3, this symbol was not visible. It is on 2.6 to 3.2, as well as 3.4+
|
135
|
+
$defs << "-DNO_RB_OBJ_INFO" if RUBY_VERSION.start_with?("2.5", "3.3")
|
136
|
+
|
134
137
|
# On older Rubies, rb_postponed_job_preregister/rb_postponed_job_trigger did not exist
|
135
138
|
$defs << "-DNO_POSTPONED_TRIGGER" if RUBY_VERSION < "3.3"
|
136
139
|
|
@@ -167,11 +170,6 @@ $defs << "-DNO_THREAD_TID" if RUBY_VERSION < "3.1"
|
|
167
170
|
# On older Rubies, there was no jit_return member on the rb_control_frame_t struct
|
168
171
|
$defs << "-DNO_JIT_RETURN" if RUBY_VERSION < "3.1"
|
169
172
|
|
170
|
-
# On older Rubies, rb_gc_force_recycle allowed to free objects in a way that
|
171
|
-
# would be invisible to free tracepoints, finalizers and without cleaning
|
172
|
-
# obj_to_id_tbl mappings.
|
173
|
-
$defs << "-DHAVE_WORKING_RB_GC_FORCE_RECYCLE" if RUBY_VERSION < "3.1"
|
174
|
-
|
175
173
|
# On older Rubies, there are no Ractors
|
176
174
|
$defs << "-DNO_RACTORS" if RUBY_VERSION < "3"
|
177
175
|
|
@@ -181,9 +179,6 @@ $defs << "-DNO_IMEMO_NAME" if RUBY_VERSION < "3"
|
|
181
179
|
# On older Rubies, objects would not move
|
182
180
|
$defs << "-DNO_T_MOVED" if RUBY_VERSION < "2.7"
|
183
181
|
|
184
|
-
# On older Rubies, there was no RUBY_SEEN_OBJ_ID flag
|
185
|
-
$defs << "-DNO_SEEN_OBJ_ID_FLAG" if RUBY_VERSION < "2.7"
|
186
|
-
|
187
182
|
# On older Rubies, rb_global_vm_lock_struct did not include the owner field
|
188
183
|
$defs << "-DNO_GVL_OWNER" if RUBY_VERSION < "2.6"
|
189
184
|
|
@@ -7,10 +7,6 @@
|
|
7
7
|
#include "libdatadog_helpers.h"
|
8
8
|
#include "time_helpers.h"
|
9
9
|
|
10
|
-
#if (defined(HAVE_WORKING_RB_GC_FORCE_RECYCLE) && ! defined(NO_SEEN_OBJ_ID_FLAG))
|
11
|
-
#define CAN_APPLY_GC_FORCE_RECYCLE_BUG_WORKAROUND
|
12
|
-
#endif
|
13
|
-
|
14
10
|
// Minimum age (in GC generations) of heap objects we want to include in heap
|
15
11
|
// recorder iterations. Object with age 0 represent objects that have yet to undergo
|
16
12
|
// a GC and, thus, may just be noise/trash at instant of iteration and are usually not
|
@@ -123,9 +119,6 @@ typedef struct {
|
|
123
119
|
// Pointer to the (potentially partial) object_record containing metadata about an ongoing recording.
|
124
120
|
// When NULL, this symbolizes an unstarted/invalid recording.
|
125
121
|
object_record *object_record;
|
126
|
-
// A flag to track whether we had to force set the RUBY_FL_SEEN_OBJ_ID flag on this object
|
127
|
-
// as part of our workaround around rb_gc_force_recycle issues.
|
128
|
-
bool did_recycle_workaround;
|
129
122
|
} recording;
|
130
123
|
|
131
124
|
struct heap_recorder {
|
@@ -342,46 +335,12 @@ void start_heap_allocation_recording(heap_recorder *heap_recorder, VALUE new_obj
|
|
342
335
|
rb_raise(rb_eRuntimeError, "Detected a bignum object id. These are not supported by heap profiling.");
|
343
336
|
}
|
344
337
|
|
345
|
-
bool did_recycle_workaround = false;
|
346
|
-
|
347
|
-
#ifdef CAN_APPLY_GC_FORCE_RECYCLE_BUG_WORKAROUND
|
348
|
-
// If we are in a ruby version that has a working rb_gc_force_recycle implementation,
|
349
|
-
// its usage may lead to an object being re-used outside of the typical GC cycle.
|
350
|
-
//
|
351
|
-
// This re-use is in theory invisible to us unless we're lucky enough to sample both
|
352
|
-
// the original object and the replacement that uses the recycled slot.
|
353
|
-
//
|
354
|
-
// In practice, we've observed (https://github.com/DataDog/dd-trace-rb/pull/3366)
|
355
|
-
// that non-noop implementations of rb_gc_force_recycle have an implementation bug
|
356
|
-
// which results in the object that re-used the recycled slot inheriting the same
|
357
|
-
// object id without setting the FL_SEEN_OBJ_ID flag. We rely on this knowledge to
|
358
|
-
// "observe" implicit frees when an object we are tracking is force-recycled.
|
359
|
-
//
|
360
|
-
// However, it may happen that we start tracking a new object and that object was
|
361
|
-
// allocated on a recycled slot. Due to the bug, this object would be missing the
|
362
|
-
// FL_SEEN_OBJ_ID flag even though it was not recycled itself. If we left it be,
|
363
|
-
// when we're doing our liveness check, the absence of the flag would trigger our
|
364
|
-
// implicit free workaround and the object would be inferred as recycled even though
|
365
|
-
// it might still be alive.
|
366
|
-
//
|
367
|
-
// Thus, if we detect that this new allocation is already missing the flag at the start
|
368
|
-
// of the heap allocation recording, we force-set it. This should be safe since we
|
369
|
-
// just called rb_obj_id on it above and the expectation is that any flaggable object
|
370
|
-
// that goes through it ends up with the flag set (as evidenced by the GC_ASSERT
|
371
|
-
// lines in https://github.com/ruby/ruby/blob/4a8d7246d15b2054eacb20f8ab3d29d39a3e7856/gc.c#L4050C14-L4050C14).
|
372
|
-
if (RB_FL_ABLE(new_obj) && !RB_FL_TEST(new_obj, RUBY_FL_SEEN_OBJ_ID)) {
|
373
|
-
RB_FL_SET(new_obj, RUBY_FL_SEEN_OBJ_ID);
|
374
|
-
did_recycle_workaround = true;
|
375
|
-
}
|
376
|
-
#endif
|
377
|
-
|
378
338
|
heap_recorder->active_recording = (recording) {
|
379
339
|
.object_record = object_record_new(FIX2LONG(ruby_obj_id), NULL, (live_object_data) {
|
380
340
|
.weight = weight * heap_recorder->sample_rate,
|
381
341
|
.class = alloc_class != NULL ? string_from_char_slice(*alloc_class) : NULL,
|
382
342
|
.alloc_gen = rb_gc_count(),
|
383
|
-
|
384
|
-
.did_recycle_workaround = did_recycle_workaround,
|
343
|
+
}),
|
385
344
|
};
|
386
345
|
}
|
387
346
|
|
@@ -685,41 +644,6 @@ static int st_object_record_update(st_data_t key, st_data_t value, st_data_t ext
|
|
685
644
|
|
686
645
|
// If we got this far, then we found a valid live object for the tracked id.
|
687
646
|
|
688
|
-
#ifdef CAN_APPLY_GC_FORCE_RECYCLE_BUG_WORKAROUND
|
689
|
-
// If we are in a ruby version that has a working rb_gc_force_recycle implementation,
|
690
|
-
// its usage may lead to an object being re-used outside of the typical GC cycle.
|
691
|
-
//
|
692
|
-
// This re-use is in theory invisible to us and would mean that the ref from which we
|
693
|
-
// collected the object_record metadata may not be the same as the current ref and
|
694
|
-
// thus any further reporting would be innacurately attributed to stale metadata.
|
695
|
-
//
|
696
|
-
// In practice, there is a way for us to notice that this happened because of a bug
|
697
|
-
// in the implementation of rb_gc_force_recycle. Our heap profiler relies on object
|
698
|
-
// ids and id2ref to detect whether objects are still alive. Turns out that when an
|
699
|
-
// object with an id is re-used via rb_gc_force_recycle, it will "inherit" the ID
|
700
|
-
// of the old object but it will NOT have the FL_SEEN_OBJ_ID as per the experiment
|
701
|
-
// in https://github.com/DataDog/dd-trace-rb/pull/3360#discussion_r1442823517
|
702
|
-
//
|
703
|
-
// Thus, if we detect that the ref we just resolved above is missing this flag, we can
|
704
|
-
// safely say re-use happened and thus treat it as an implicit free of the object
|
705
|
-
// we were tracking (the original one which got recycled).
|
706
|
-
if (RB_FL_ABLE(ref) && !RB_FL_TEST(ref, RUBY_FL_SEEN_OBJ_ID)) {
|
707
|
-
|
708
|
-
// NOTE: We don't really need to set this flag for heap recorder to work correctly
|
709
|
-
// but doing so partially mitigates a bug in runtimes with working rb_gc_force_recycle
|
710
|
-
// which leads to broken invariants and leaking of entries in obj_to_id and id_to_obj
|
711
|
-
// tables in objspace. We already do the same thing when we sample a recycled object,
|
712
|
-
// here we apply it as well to objects that replace recycled objects that were being
|
713
|
-
// tracked. More details in https://github.com/DataDog/dd-trace-rb/pull/3366
|
714
|
-
RB_FL_SET(ref, RUBY_FL_SEEN_OBJ_ID);
|
715
|
-
|
716
|
-
on_committed_object_record_cleanup(recorder, record);
|
717
|
-
recorder->stats_last_update.objects_dead++;
|
718
|
-
return ST_DELETE;
|
719
|
-
}
|
720
|
-
|
721
|
-
#endif
|
722
|
-
|
723
647
|
if (
|
724
648
|
recorder->size_enabled &&
|
725
649
|
recorder->update_include_old && // We only update sizes when doing a full update
|
@@ -732,6 +656,10 @@ static int st_object_record_update(st_data_t key, st_data_t value, st_data_t ext
|
|
732
656
|
record->object_data.is_frozen = RB_OBJ_FROZEN(ref);
|
733
657
|
}
|
734
658
|
|
659
|
+
// Ensure that ref is kept on the stack so the Ruby garbage collector does not try to clean up the object before this
|
660
|
+
// point.
|
661
|
+
RB_GC_GUARD(ref);
|
662
|
+
|
735
663
|
recorder->stats_last_update.objects_alive++;
|
736
664
|
if (record->object_data.is_frozen) {
|
737
665
|
recorder->stats_last_update.objects_frozen++;
|
@@ -803,18 +731,12 @@ static int update_object_record_entry(DDTRACE_UNUSED st_data_t *key, st_data_t *
|
|
803
731
|
object_record *new_object_record = recording.object_record;
|
804
732
|
if (existing) {
|
805
733
|
object_record *existing_record = (object_record*) (*value);
|
806
|
-
|
807
|
-
|
808
|
-
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
// This is not supposed to happen, raising...
|
813
|
-
VALUE existing_inspect = object_record_inspect(existing_record);
|
814
|
-
VALUE new_inspect = object_record_inspect(new_object_record);
|
815
|
-
rb_raise(rb_eRuntimeError, "Object ids are supposed to be unique. We got 2 allocation recordings with "
|
816
|
-
"the same id. previous=%"PRIsVALUE" new=%"PRIsVALUE, existing_inspect, new_inspect);
|
817
|
-
}
|
734
|
+
|
735
|
+
// This is not supposed to happen, raising...
|
736
|
+
VALUE existing_inspect = object_record_inspect(existing_record);
|
737
|
+
VALUE new_inspect = object_record_inspect(new_object_record);
|
738
|
+
rb_raise(rb_eRuntimeError, "Object ids are supposed to be unique. We got 2 allocation recordings with "
|
739
|
+
"the same id. previous=%"PRIsVALUE" new=%"PRIsVALUE, existing_inspect, new_inspect);
|
818
740
|
}
|
819
741
|
// Always carry on with the update, we want the new record to be there at the end
|
820
742
|
(*value) = (st_data_t) new_object_record;
|
@@ -158,7 +158,7 @@ bool is_current_thread_holding_the_gvl(void) {
|
|
158
158
|
//
|
159
159
|
// Thus an incorrect `is_current_thread_holding_the_gvl` result may lead to issues inside `rb_postponed_job_register_one`.
|
160
160
|
//
|
161
|
-
// For this reason we
|
161
|
+
// For this reason we default to use the "no signals workaround" on Ruby 2.5 by default, and we print a
|
162
162
|
// warning when customers force-enable it.
|
163
163
|
bool gvl_acquired = vm->gvl.acquired != 0;
|
164
164
|
rb_thread_t *current_owner = vm->running_thread;
|
@@ -587,16 +587,13 @@ int ddtrace_rb_profile_frames(VALUE thread, int start, int limit, frame_info *st
|
|
587
587
|
// Taken from upstream vm_insnhelper.c at commit 5f10bd634fb6ae8f74a4ea730176233b0ca96954 (March 2022, Ruby 3.2 trunk)
|
588
588
|
// Copyright (C) 2007 Koichi Sasada
|
589
589
|
// to support our custom rb_profile_frames (see above)
|
590
|
-
// Modifications:
|
590
|
+
// Modifications:
|
591
|
+
// * Removed debug checks (they were ifdef'd out anyway)
|
591
592
|
static rb_callable_method_entry_t *
|
592
593
|
check_method_entry(VALUE obj, int can_be_svar)
|
593
594
|
{
|
594
595
|
if (obj == Qfalse) return NULL;
|
595
596
|
|
596
|
-
#if VM_CHECK_MODE > 0
|
597
|
-
if (!RB_TYPE_P(obj, T_IMEMO)) rb_bug("check_method_entry: unknown type: %s", rb_obj_info(obj));
|
598
|
-
#endif
|
599
|
-
|
600
597
|
switch (imemo_type(obj)) {
|
601
598
|
case imemo_ment:
|
602
599
|
return (rb_callable_method_entry_t *)obj;
|
@@ -608,9 +605,6 @@ check_method_entry(VALUE obj, int can_be_svar)
|
|
608
605
|
}
|
609
606
|
// fallthrough
|
610
607
|
default:
|
611
|
-
#if VM_CHECK_MODE > 0
|
612
|
-
rb_bug("check_method_entry: svar should not be there:");
|
613
|
-
#endif
|
614
608
|
return NULL;
|
615
609
|
}
|
616
610
|
}
|
@@ -37,6 +37,7 @@ static VALUE _native_trigger_holding_the_gvl_signal_handler_on(DDTRACE_UNUSED VA
|
|
37
37
|
static VALUE _native_enforce_success(DDTRACE_UNUSED VALUE _self, VALUE syserr_errno, VALUE with_gvl);
|
38
38
|
static void *trigger_enforce_success(void *trigger_args);
|
39
39
|
static VALUE _native_malloc_stats(DDTRACE_UNUSED VALUE _self);
|
40
|
+
static VALUE _native_safe_object_info(DDTRACE_UNUSED VALUE _self, VALUE obj);
|
40
41
|
|
41
42
|
void DDTRACE_EXPORT Init_datadog_profiling_native_extension(void) {
|
42
43
|
VALUE datadog_module = rb_define_module("Datadog");
|
@@ -72,6 +73,7 @@ void DDTRACE_EXPORT Init_datadog_profiling_native_extension(void) {
|
|
72
73
|
rb_define_singleton_method(testing_module, "_native_trigger_holding_the_gvl_signal_handler_on", _native_trigger_holding_the_gvl_signal_handler_on, 1);
|
73
74
|
rb_define_singleton_method(testing_module, "_native_enforce_success", _native_enforce_success, 2);
|
74
75
|
rb_define_singleton_method(testing_module, "_native_malloc_stats", _native_malloc_stats, 0);
|
76
|
+
rb_define_singleton_method(testing_module, "_native_safe_object_info", _native_safe_object_info, 1);
|
75
77
|
}
|
76
78
|
|
77
79
|
static VALUE native_working_p(DDTRACE_UNUSED VALUE _self) {
|
@@ -265,3 +267,7 @@ static VALUE _native_malloc_stats(DDTRACE_UNUSED VALUE _self) {
|
|
265
267
|
return Qfalse;
|
266
268
|
#endif
|
267
269
|
}
|
270
|
+
|
271
|
+
static VALUE _native_safe_object_info(DDTRACE_UNUSED VALUE _self, VALUE obj) {
|
272
|
+
return rb_str_new_cstr(safe_object_info(obj));
|
273
|
+
}
|
@@ -3,6 +3,7 @@
|
|
3
3
|
|
4
4
|
#include "ruby_helpers.h"
|
5
5
|
#include "private_vm_api_access.h"
|
6
|
+
#include "extconf.h"
|
6
7
|
|
7
8
|
// The following global variables are initialized at startup to save expensive lookups later.
|
8
9
|
// They are not expected to be mutated outside of init.
|
@@ -219,17 +220,26 @@ static bool ruby_is_obj_with_class(VALUE obj) {
|
|
219
220
|
return false;
|
220
221
|
}
|
221
222
|
|
222
|
-
//
|
223
|
+
// This function is not present in the VM headers, but is a public symbol that can be invoked.
|
223
224
|
int rb_objspace_internal_object_p(VALUE obj);
|
224
|
-
|
225
|
+
|
226
|
+
#ifdef NO_RB_OBJ_INFO
|
227
|
+
const char* safe_object_info(DDTRACE_UNUSED VALUE obj) { return "(No rb_obj_info for current Ruby)"; }
|
228
|
+
#else
|
229
|
+
// This function is a public symbol, but not on all Rubies; `safe_object_info` below abstracts this, and
|
230
|
+
// should be used instead.
|
231
|
+
const char *rb_obj_info(VALUE obj);
|
232
|
+
|
233
|
+
const char* safe_object_info(VALUE obj) { return rb_obj_info(obj); }
|
234
|
+
#endif
|
225
235
|
|
226
236
|
VALUE ruby_safe_inspect(VALUE obj) {
|
227
237
|
if (!ruby_is_obj_with_class(obj)) return rb_str_new_cstr("(Not an object)");
|
228
|
-
if (rb_objspace_internal_object_p(obj)) return rb_sprintf("(VM Internal, %s)",
|
238
|
+
if (rb_objspace_internal_object_p(obj)) return rb_sprintf("(VM Internal, %s)", safe_object_info(obj));
|
229
239
|
// @ivoanjo: I saw crashes on Ruby 3.1.4 when trying to #inspect matchdata objects. I'm not entirely sure why this
|
230
240
|
// is needed, but since we only use this method for debug purposes I put in this alternative and decided not to
|
231
241
|
// dig deeper.
|
232
|
-
if (rb_type(obj) == RUBY_T_MATCH) return rb_sprintf("(MatchData, %s)",
|
242
|
+
if (rb_type(obj) == RUBY_T_MATCH) return rb_sprintf("(MatchData, %s)", safe_object_info(obj));
|
233
243
|
if (rb_respond_to(obj, inspect_id)) return rb_sprintf("%+"PRIsVALUE, obj);
|
234
244
|
if (rb_respond_to(obj, to_s_id)) return rb_sprintf("%"PRIsVALUE, obj);
|
235
245
|
|
@@ -90,3 +90,7 @@ size_t ruby_obj_memsize_of(VALUE obj);
|
|
90
90
|
// return a string with the result of that call. Elsif the object responds to
|
91
91
|
// 'to_s', return a string with the result of that call. Otherwise, return Qnil.
|
92
92
|
VALUE ruby_safe_inspect(VALUE obj);
|
93
|
+
|
94
|
+
// You probably want ruby_safe_inspect instead; this is a lower-level dependency
|
95
|
+
// of it, that's being exposed here just to facilitate testing.
|
96
|
+
const char* safe_object_info(VALUE obj);
|
@@ -258,8 +258,6 @@ static VALUE _native_check_heap_hashes(DDTRACE_UNUSED VALUE _self, VALUE locatio
|
|
258
258
|
static VALUE _native_start_fake_slow_heap_serialization(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance);
|
259
259
|
static VALUE _native_end_fake_slow_heap_serialization(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance);
|
260
260
|
static VALUE _native_debug_heap_recorder(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance);
|
261
|
-
static VALUE _native_gc_force_recycle(DDTRACE_UNUSED VALUE _self, VALUE obj);
|
262
|
-
static VALUE _native_has_seen_id_flag(DDTRACE_UNUSED VALUE _self, VALUE obj);
|
263
261
|
static VALUE _native_stats(DDTRACE_UNUSED VALUE self, VALUE instance);
|
264
262
|
static VALUE build_profile_stats(profile_slot *slot, long serialization_time_ns, long heap_iteration_prep_time_ns, long heap_profile_build_time_ns);
|
265
263
|
static VALUE _native_is_object_recorded(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance, VALUE object_id);
|
@@ -297,10 +295,6 @@ void stack_recorder_init(VALUE profiling_module) {
|
|
297
295
|
_native_end_fake_slow_heap_serialization, 1);
|
298
296
|
rb_define_singleton_method(testing_module, "_native_debug_heap_recorder",
|
299
297
|
_native_debug_heap_recorder, 1);
|
300
|
-
rb_define_singleton_method(testing_module, "_native_gc_force_recycle",
|
301
|
-
_native_gc_force_recycle, 1);
|
302
|
-
rb_define_singleton_method(testing_module, "_native_has_seen_id_flag",
|
303
|
-
_native_has_seen_id_flag, 1);
|
304
298
|
rb_define_singleton_method(testing_module, "_native_is_object_recorded?", _native_is_object_recorded, 2);
|
305
299
|
rb_define_singleton_method(testing_module, "_native_heap_recorder_reset_last_update", _native_heap_recorder_reset_last_update, 1);
|
306
300
|
rb_define_singleton_method(testing_module, "_native_recorder_after_gc_step", _native_recorder_after_gc_step, 1);
|
@@ -1006,34 +1000,6 @@ static VALUE _native_debug_heap_recorder(DDTRACE_UNUSED VALUE _self, VALUE recor
|
|
1006
1000
|
return heap_recorder_testonly_debug(state->heap_recorder);
|
1007
1001
|
}
|
1008
1002
|
|
1009
|
-
#pragma GCC diagnostic push
|
1010
|
-
// rb_gc_force_recycle was deprecated in latest versions of Ruby and is a noop.
|
1011
|
-
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
1012
|
-
#pragma GCC diagnostic ignored "-Wunused-parameter"
|
1013
|
-
// This method exists only to enable testing Datadog::Profiling::StackRecorder behavior using RSpec.
|
1014
|
-
// It SHOULD NOT be used for other purposes.
|
1015
|
-
static VALUE _native_gc_force_recycle(DDTRACE_UNUSED VALUE _self, VALUE obj) {
|
1016
|
-
#ifdef HAVE_WORKING_RB_GC_FORCE_RECYCLE
|
1017
|
-
rb_gc_force_recycle(obj);
|
1018
|
-
#endif
|
1019
|
-
return Qnil;
|
1020
|
-
}
|
1021
|
-
#pragma GCC diagnostic pop
|
1022
|
-
|
1023
|
-
// This method exists only to enable testing Datadog::Profiling::StackRecorder behavior using RSpec.
|
1024
|
-
// It SHOULD NOT be used for other purposes.
|
1025
|
-
static VALUE _native_has_seen_id_flag(DDTRACE_UNUSED VALUE _self, VALUE obj) {
|
1026
|
-
#ifndef NO_SEEN_OBJ_ID_FLAG
|
1027
|
-
if (RB_FL_TEST(obj, RUBY_FL_SEEN_OBJ_ID)) {
|
1028
|
-
return Qtrue;
|
1029
|
-
} else {
|
1030
|
-
return Qfalse;
|
1031
|
-
}
|
1032
|
-
#else
|
1033
|
-
return Qfalse;
|
1034
|
-
#endif
|
1035
|
-
}
|
1036
|
-
|
1037
1003
|
static VALUE _native_stats(DDTRACE_UNUSED VALUE self, VALUE recorder_instance) {
|
1038
1004
|
struct stack_recorder_state *state;
|
1039
1005
|
TypedData_Get_Struct(recorder_instance, struct stack_recorder_state, &stack_recorder_typed_data, state);
|
@@ -8,7 +8,7 @@ module Datadog
|
|
8
8
|
module LibdatadogExtconfHelpers
|
9
9
|
# Used to make sure the correct gem version gets loaded, as extconf.rb does not get run with "bundle exec" and thus
|
10
10
|
# may see multiple libdatadog versions. See https://github.com/DataDog/dd-trace-rb/pull/2531 for the horror story.
|
11
|
-
LIBDATADOG_VERSION = '~> 14.1.
|
11
|
+
LIBDATADOG_VERSION = '~> 14.3.1.1.0'
|
12
12
|
|
13
13
|
# Used as an workaround for a limitation with how dynamic linking works in environments where the datadog gem and
|
14
14
|
# libdatadog are moved after the extension gets compiled.
|
@@ -3,7 +3,6 @@
|
|
3
3
|
require_relative 'processor'
|
4
4
|
require_relative 'processor/rule_merger'
|
5
5
|
require_relative 'processor/rule_loader'
|
6
|
-
require_relative 'processor/actions'
|
7
6
|
|
8
7
|
module Datadog
|
9
8
|
module AppSec
|
@@ -52,10 +51,6 @@ module Datadog
|
|
52
51
|
)
|
53
52
|
return nil unless rules
|
54
53
|
|
55
|
-
actions = rules['actions']
|
56
|
-
|
57
|
-
AppSec::Processor::Actions.merge(actions) if actions
|
58
|
-
|
59
54
|
data = AppSec::Processor::RuleLoader.load_data(
|
60
55
|
ip_denylist: settings.appsec.ip_denylist,
|
61
56
|
user_id_denylist: settings.appsec.user_id_denylist,
|
@@ -84,10 +79,8 @@ module Datadog
|
|
84
79
|
@mutex = Mutex.new
|
85
80
|
end
|
86
81
|
|
87
|
-
def reconfigure(ruleset:,
|
82
|
+
def reconfigure(ruleset:, telemetry:)
|
88
83
|
@mutex.synchronize do
|
89
|
-
AppSec::Processor::Actions.merge(actions)
|
90
|
-
|
91
84
|
new = Processor.new(ruleset: ruleset, telemetry: telemetry)
|
92
85
|
|
93
86
|
if new && new.ready?
|