graphql 2.2.17 → 2.5.16
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/lib/generators/graphql/install/mutation_root_generator.rb +2 -2
- data/lib/generators/graphql/install_generator.rb +46 -0
- data/lib/generators/graphql/orm_mutations_base.rb +1 -1
- data/lib/generators/graphql/templates/base_resolver.erb +2 -0
- data/lib/generators/graphql/templates/schema.erb +3 -0
- data/lib/generators/graphql/type_generator.rb +1 -1
- data/lib/graphql/analysis/analyzer.rb +90 -0
- data/lib/graphql/analysis/field_usage.rb +82 -0
- data/lib/graphql/analysis/max_query_complexity.rb +20 -0
- data/lib/graphql/analysis/max_query_depth.rb +20 -0
- data/lib/graphql/analysis/query_complexity.rb +263 -0
- data/lib/graphql/analysis/{ast/query_depth.rb → query_depth.rb} +23 -25
- data/lib/graphql/analysis/visitor.rb +280 -0
- data/lib/graphql/analysis.rb +95 -1
- data/lib/graphql/autoload.rb +38 -0
- data/lib/graphql/backtrace/table.rb +118 -55
- data/lib/graphql/backtrace.rb +1 -19
- data/lib/graphql/current.rb +57 -0
- data/lib/graphql/dashboard/detailed_traces.rb +47 -0
- data/lib/graphql/dashboard/installable.rb +22 -0
- data/lib/graphql/dashboard/limiters.rb +93 -0
- data/lib/graphql/dashboard/operation_store.rb +199 -0
- data/lib/graphql/dashboard/statics/bootstrap-5.3.3.min.css +6 -0
- data/lib/graphql/dashboard/statics/bootstrap-5.3.3.min.js +7 -0
- data/lib/graphql/dashboard/statics/charts.min.css +1 -0
- data/lib/graphql/dashboard/statics/dashboard.css +30 -0
- data/lib/graphql/dashboard/statics/dashboard.js +143 -0
- data/lib/graphql/dashboard/statics/header-icon.png +0 -0
- data/lib/graphql/dashboard/statics/icon.png +0 -0
- data/lib/graphql/dashboard/subscriptions.rb +96 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/detailed_traces/traces/index.html.erb +45 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/landings/show.html.erb +18 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/limiters/limiters/show.html.erb +62 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/not_installed.html.erb +18 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/_form.html.erb +23 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/edit.html.erb +21 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/index.html.erb +69 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/new.html.erb +7 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/index.html.erb +39 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/show.html.erb +32 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/index.html.erb +81 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/show.html.erb +71 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/subscriptions/show.html.erb +41 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/index.html.erb +55 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/show.html.erb +40 -0
- data/lib/graphql/dashboard/views/layouts/graphql/dashboard/application.html.erb +108 -0
- data/lib/graphql/dashboard.rb +158 -0
- data/lib/graphql/dataloader/active_record_association_source.rb +84 -0
- data/lib/graphql/dataloader/active_record_source.rb +47 -0
- data/lib/graphql/dataloader/async_dataloader.rb +46 -19
- data/lib/graphql/dataloader/null_dataloader.rb +51 -10
- data/lib/graphql/dataloader/source.rb +20 -9
- data/lib/graphql/dataloader.rb +153 -45
- data/lib/graphql/date_encoding_error.rb +1 -1
- data/lib/graphql/dig.rb +2 -1
- data/lib/graphql/execution/interpreter/argument_value.rb +5 -1
- data/lib/graphql/execution/interpreter/arguments_cache.rb +5 -10
- data/lib/graphql/execution/interpreter/resolve.rb +23 -25
- data/lib/graphql/execution/interpreter/runtime/graphql_result.rb +63 -5
- data/lib/graphql/execution/interpreter/runtime.rb +321 -222
- data/lib/graphql/execution/interpreter.rb +23 -30
- data/lib/graphql/execution/lookahead.rb +18 -11
- data/lib/graphql/execution/multiplex.rb +6 -5
- data/lib/graphql/introspection/directive_location_enum.rb +1 -1
- data/lib/graphql/introspection/directive_type.rb +1 -1
- data/lib/graphql/introspection/entry_points.rb +2 -2
- data/lib/graphql/introspection/field_type.rb +1 -1
- data/lib/graphql/introspection/schema_type.rb +6 -11
- data/lib/graphql/introspection/type_type.rb +5 -5
- data/lib/graphql/invalid_name_error.rb +1 -1
- data/lib/graphql/invalid_null_error.rb +20 -17
- data/lib/graphql/language/cache.rb +13 -0
- data/lib/graphql/language/comment.rb +18 -0
- data/lib/graphql/language/document_from_schema_definition.rb +64 -35
- data/lib/graphql/language/lexer.rb +72 -42
- data/lib/graphql/language/nodes.rb +93 -52
- data/lib/graphql/language/parser.rb +168 -61
- data/lib/graphql/language/printer.rb +31 -15
- data/lib/graphql/language/sanitized_printer.rb +1 -1
- data/lib/graphql/language.rb +61 -1
- data/lib/graphql/pagination/connection.rb +1 -1
- data/lib/graphql/query/context/scoped_context.rb +1 -1
- data/lib/graphql/query/context.rb +46 -47
- data/lib/graphql/query/null_context.rb +3 -5
- data/lib/graphql/query/partial.rb +179 -0
- data/lib/graphql/query/validation_pipeline.rb +2 -2
- data/lib/graphql/query/variable_validation_error.rb +1 -1
- data/lib/graphql/query.rb +123 -69
- data/lib/graphql/railtie.rb +7 -0
- data/lib/graphql/rubocop/graphql/base_cop.rb +1 -1
- data/lib/graphql/rubocop/graphql/field_type_in_block.rb +144 -0
- data/lib/graphql/rubocop/graphql/root_types_in_block.rb +38 -0
- data/lib/graphql/rubocop.rb +2 -0
- data/lib/graphql/schema/addition.rb +26 -13
- data/lib/graphql/schema/always_visible.rb +7 -2
- data/lib/graphql/schema/argument.rb +57 -8
- data/lib/graphql/schema/build_from_definition.rb +116 -49
- data/lib/graphql/schema/directive/flagged.rb +4 -2
- data/lib/graphql/schema/directive.rb +54 -2
- data/lib/graphql/schema/enum.rb +107 -24
- data/lib/graphql/schema/enum_value.rb +10 -2
- data/lib/graphql/schema/field/connection_extension.rb +1 -1
- data/lib/graphql/schema/field/scope_extension.rb +1 -1
- data/lib/graphql/schema/field.rb +134 -45
- data/lib/graphql/schema/field_extension.rb +1 -1
- data/lib/graphql/schema/has_single_input_argument.rb +6 -2
- data/lib/graphql/schema/input_object.rb +122 -64
- data/lib/graphql/schema/interface.rb +23 -5
- data/lib/graphql/schema/introspection_system.rb +6 -17
- data/lib/graphql/schema/late_bound_type.rb +4 -0
- data/lib/graphql/schema/list.rb +3 -3
- data/lib/graphql/schema/loader.rb +3 -2
- data/lib/graphql/schema/member/base_dsl_methods.rb +15 -0
- data/lib/graphql/schema/member/has_arguments.rb +44 -58
- data/lib/graphql/schema/member/has_dataloader.rb +62 -0
- data/lib/graphql/schema/member/has_deprecation_reason.rb +15 -0
- data/lib/graphql/schema/member/has_directives.rb +4 -4
- data/lib/graphql/schema/member/has_fields.rb +26 -6
- data/lib/graphql/schema/member/has_interfaces.rb +6 -6
- data/lib/graphql/schema/member/has_unresolved_type_error.rb +5 -1
- data/lib/graphql/schema/member/has_validators.rb +1 -1
- data/lib/graphql/schema/member/relay_shortcuts.rb +1 -1
- data/lib/graphql/schema/member/type_system_helpers.rb +17 -4
- data/lib/graphql/schema/member.rb +1 -0
- data/lib/graphql/schema/mutation.rb +7 -0
- data/lib/graphql/schema/object.rb +25 -8
- data/lib/graphql/schema/printer.rb +1 -0
- data/lib/graphql/schema/ractor_shareable.rb +79 -0
- data/lib/graphql/schema/relay_classic_mutation.rb +0 -1
- data/lib/graphql/schema/resolver.rb +29 -23
- data/lib/graphql/schema/scalar.rb +1 -6
- data/lib/graphql/schema/subscription.rb +52 -6
- data/lib/graphql/schema/timeout.rb +19 -2
- data/lib/graphql/schema/type_expression.rb +2 -2
- data/lib/graphql/schema/union.rb +1 -1
- data/lib/graphql/schema/validator/all_validator.rb +62 -0
- data/lib/graphql/schema/validator/required_validator.rb +92 -11
- data/lib/graphql/schema/validator.rb +3 -1
- data/lib/graphql/schema/visibility/migration.rb +188 -0
- data/lib/graphql/schema/visibility/profile.rb +445 -0
- data/lib/graphql/schema/visibility/visit.rb +190 -0
- data/lib/graphql/schema/visibility.rb +311 -0
- data/lib/graphql/schema/warden.rb +190 -20
- data/lib/graphql/schema.rb +695 -167
- data/lib/graphql/static_validation/all_rules.rb +2 -2
- data/lib/graphql/static_validation/base_visitor.rb +6 -5
- data/lib/graphql/static_validation/literal_validator.rb +4 -4
- data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +1 -1
- data/lib/graphql/static_validation/rules/argument_names_are_unique.rb +1 -1
- data/lib/graphql/static_validation/rules/arguments_are_defined.rb +3 -2
- data/lib/graphql/static_validation/rules/directives_are_defined.rb +3 -3
- data/lib/graphql/static_validation/rules/directives_are_in_valid_locations.rb +2 -0
- data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +12 -2
- data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +47 -13
- data/lib/graphql/static_validation/rules/fields_will_merge.rb +88 -25
- data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +10 -2
- data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +3 -3
- data/lib/graphql/static_validation/rules/fragment_types_exist.rb +12 -2
- data/lib/graphql/static_validation/rules/fragments_are_on_composite_types.rb +1 -1
- data/lib/graphql/static_validation/rules/mutation_root_exists.rb +1 -1
- data/lib/graphql/static_validation/rules/no_definitions_are_present.rb +1 -1
- data/lib/graphql/static_validation/rules/not_single_subscription_error.rb +25 -0
- data/lib/graphql/static_validation/rules/query_root_exists.rb +1 -1
- data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +4 -4
- data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +3 -3
- data/lib/graphql/static_validation/rules/subscription_root_exists_and_single_subscription_selection.rb +26 -0
- data/lib/graphql/static_validation/rules/unique_directives_per_location.rb +7 -3
- data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +18 -27
- data/lib/graphql/static_validation/rules/variable_names_are_unique.rb +1 -1
- data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +2 -2
- data/lib/graphql/static_validation/rules/variables_are_input_types.rb +11 -2
- data/lib/graphql/static_validation/validation_context.rb +18 -2
- data/lib/graphql/static_validation/validator.rb +6 -1
- data/lib/graphql/subscriptions/action_cable_subscriptions.rb +5 -3
- data/lib/graphql/subscriptions/broadcast_analyzer.rb +11 -5
- data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +12 -10
- data/lib/graphql/subscriptions/event.rb +13 -2
- data/lib/graphql/subscriptions/serialize.rb +1 -1
- data/lib/graphql/subscriptions.rb +7 -5
- data/lib/graphql/testing/helpers.rb +48 -16
- data/lib/graphql/testing/mock_action_cable.rb +111 -0
- data/lib/graphql/testing.rb +1 -0
- data/lib/graphql/tracing/active_support_notifications_trace.rb +14 -3
- data/lib/graphql/tracing/active_support_notifications_tracing.rb +1 -1
- data/lib/graphql/tracing/appoptics_trace.rb +5 -1
- data/lib/graphql/tracing/appoptics_tracing.rb +7 -0
- data/lib/graphql/tracing/appsignal_trace.rb +32 -59
- data/lib/graphql/tracing/appsignal_tracing.rb +2 -0
- data/lib/graphql/tracing/call_legacy_tracers.rb +66 -0
- data/lib/graphql/tracing/data_dog_trace.rb +46 -162
- data/lib/graphql/tracing/data_dog_tracing.rb +2 -0
- data/lib/graphql/tracing/detailed_trace/memory_backend.rb +60 -0
- data/lib/graphql/tracing/detailed_trace/redis_backend.rb +72 -0
- data/lib/graphql/tracing/detailed_trace.rb +141 -0
- data/lib/graphql/tracing/legacy_hooks_trace.rb +1 -0
- data/lib/graphql/tracing/legacy_trace.rb +4 -61
- data/lib/graphql/tracing/monitor_trace.rb +283 -0
- data/lib/graphql/tracing/new_relic_trace.rb +47 -54
- data/lib/graphql/tracing/new_relic_tracing.rb +2 -0
- data/lib/graphql/tracing/notifications_trace.rb +183 -37
- data/lib/graphql/tracing/notifications_tracing.rb +2 -0
- data/lib/graphql/tracing/null_trace.rb +9 -0
- data/lib/graphql/tracing/perfetto_trace/trace.proto +141 -0
- data/lib/graphql/tracing/perfetto_trace/trace_pb.rb +33 -0
- data/lib/graphql/tracing/perfetto_trace.rb +818 -0
- data/lib/graphql/tracing/platform_tracing.rb +1 -1
- data/lib/graphql/tracing/prometheus_trace/graphql_collector.rb +2 -0
- data/lib/graphql/tracing/prometheus_trace.rb +73 -73
- data/lib/graphql/tracing/prometheus_tracing.rb +2 -0
- data/lib/graphql/tracing/scout_trace.rb +32 -58
- data/lib/graphql/tracing/scout_tracing.rb +2 -0
- data/lib/graphql/tracing/sentry_trace.rb +64 -98
- data/lib/graphql/tracing/statsd_trace.rb +33 -45
- data/lib/graphql/tracing/statsd_tracing.rb +2 -0
- data/lib/graphql/tracing/trace.rb +111 -1
- data/lib/graphql/tracing.rb +31 -30
- data/lib/graphql/type_kinds.rb +2 -1
- data/lib/graphql/types/relay/connection_behaviors.rb +12 -2
- data/lib/graphql/types/relay/edge_behaviors.rb +11 -1
- data/lib/graphql/types/relay/page_info_behaviors.rb +4 -0
- data/lib/graphql/types.rb +18 -11
- data/lib/graphql/unauthorized_enum_value_error.rb +13 -0
- data/lib/graphql/version.rb +1 -1
- data/lib/graphql.rb +64 -54
- metadata +197 -22
- data/lib/graphql/analysis/ast/analyzer.rb +0 -91
- data/lib/graphql/analysis/ast/field_usage.rb +0 -82
- data/lib/graphql/analysis/ast/max_query_complexity.rb +0 -22
- data/lib/graphql/analysis/ast/max_query_depth.rb +0 -22
- data/lib/graphql/analysis/ast/query_complexity.rb +0 -182
- data/lib/graphql/analysis/ast/visitor.rb +0 -276
- data/lib/graphql/analysis/ast.rb +0 -94
- data/lib/graphql/backtrace/inspect_result.rb +0 -50
- data/lib/graphql/backtrace/trace.rb +0 -93
- data/lib/graphql/backtrace/tracer.rb +0 -80
- data/lib/graphql/language/token.rb +0 -34
- data/lib/graphql/schema/invalid_type_error.rb +0 -7
- data/lib/graphql/schema/null_mask.rb +0 -11
- data/lib/graphql/static_validation/rules/subscription_root_exists.rb +0 -17
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GraphQL
|
|
4
|
+
module Tracing
|
|
5
|
+
class DetailedTrace
|
|
6
|
+
# An in-memory trace storage backend. Suitable for testing and development only.
|
|
7
|
+
# It won't work for multi-process deployments and everything is erased when the app is restarted.
|
|
8
|
+
class MemoryBackend
|
|
9
|
+
def initialize(limit: nil)
|
|
10
|
+
@limit = limit
|
|
11
|
+
@traces = {}
|
|
12
|
+
@next_id = 0
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def traces(last:, before:)
|
|
16
|
+
page = []
|
|
17
|
+
@traces.values.reverse_each do |trace|
|
|
18
|
+
if page.size == last
|
|
19
|
+
break
|
|
20
|
+
elsif before.nil? || trace.begin_ms < before
|
|
21
|
+
page << trace
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
page
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def find_trace(id)
|
|
28
|
+
@traces[id]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def delete_trace(id)
|
|
32
|
+
@traces.delete(id.to_i)
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def delete_all_traces
|
|
37
|
+
@traces.clear
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def save_trace(operation_name, duration, begin_ms, trace_data)
|
|
42
|
+
id = @next_id
|
|
43
|
+
@next_id += 1
|
|
44
|
+
@traces[id] = DetailedTrace::StoredTrace.new(
|
|
45
|
+
id: id,
|
|
46
|
+
operation_name: operation_name,
|
|
47
|
+
duration_ms: duration,
|
|
48
|
+
begin_ms: begin_ms,
|
|
49
|
+
trace_data: trace_data
|
|
50
|
+
)
|
|
51
|
+
if @limit && @traces.size > @limit
|
|
52
|
+
del_keys = @traces.keys[0...-@limit]
|
|
53
|
+
del_keys.each { |k| @traces.delete(k) }
|
|
54
|
+
end
|
|
55
|
+
id
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GraphQL
|
|
4
|
+
module Tracing
|
|
5
|
+
class DetailedTrace
|
|
6
|
+
class RedisBackend
|
|
7
|
+
KEY_PREFIX = "gql:trace:"
|
|
8
|
+
def initialize(redis:, limit: nil)
|
|
9
|
+
@redis = redis
|
|
10
|
+
@key = KEY_PREFIX + "traces"
|
|
11
|
+
@remrangebyrank_limit = limit ? -limit - 1 : nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def traces(last:, before:)
|
|
15
|
+
before = case before
|
|
16
|
+
when Numeric
|
|
17
|
+
"(#{before}"
|
|
18
|
+
when nil
|
|
19
|
+
"+inf"
|
|
20
|
+
end
|
|
21
|
+
str_pairs = @redis.zrange(@key, before, 0, byscore: true, rev: true, limit: [0, last || 100], withscores: true)
|
|
22
|
+
str_pairs.map do |(str_data, score)|
|
|
23
|
+
entry_to_trace(score, str_data)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def delete_trace(id)
|
|
28
|
+
@redis.zremrangebyscore(@key, id, id)
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def delete_all_traces
|
|
33
|
+
@redis.del(@key)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def find_trace(id)
|
|
37
|
+
str_data = @redis.zrange(@key, id, id, byscore: true).first
|
|
38
|
+
if str_data.nil?
|
|
39
|
+
nil
|
|
40
|
+
else
|
|
41
|
+
entry_to_trace(id, str_data)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def save_trace(operation_name, duration_ms, begin_ms, trace_data)
|
|
46
|
+
id = begin_ms
|
|
47
|
+
data = JSON.dump({ "o" => operation_name, "d" => duration_ms, "b" => begin_ms, "t" => Base64.encode64(trace_data) })
|
|
48
|
+
@redis.pipelined do |pipeline|
|
|
49
|
+
pipeline.zadd(@key, id, data)
|
|
50
|
+
if @remrangebyrank_limit
|
|
51
|
+
pipeline.zremrangebyrank(@key, 0, @remrangebyrank_limit)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
id
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def entry_to_trace(id, json_str)
|
|
60
|
+
data = JSON.parse(json_str)
|
|
61
|
+
StoredTrace.new(
|
|
62
|
+
id: id,
|
|
63
|
+
operation_name: data["o"],
|
|
64
|
+
duration_ms: data["d"].to_f,
|
|
65
|
+
begin_ms: data["b"].to_i,
|
|
66
|
+
trace_data: Base64.decode64(data["t"]),
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "graphql/tracing/detailed_trace/memory_backend"
|
|
3
|
+
require "graphql/tracing/detailed_trace/redis_backend"
|
|
4
|
+
|
|
5
|
+
module GraphQL
|
|
6
|
+
module Tracing
|
|
7
|
+
# `DetailedTrace` can make detailed profiles for a subset of production traffic.
|
|
8
|
+
#
|
|
9
|
+
# When `MySchema.detailed_trace?(query)` returns `true`, a profiler-specific `trace_mode: ...` will be used for the query,
|
|
10
|
+
# overriding the one in `context[:trace_mode]`.
|
|
11
|
+
#
|
|
12
|
+
# By default, the detailed tracer calls `.inspect` on application objects returned from fields. You can customize
|
|
13
|
+
# this behavior by extending {DetailedTrace} and overriding {#inspect_object}. You can opt out of debug annotations
|
|
14
|
+
# entirely with `use ..., debug: false` or for a single query with `context: { detailed_trace_debug: false }`.
|
|
15
|
+
#
|
|
16
|
+
# __Redis__: The sampler stores its results in a provided Redis database. Depending on your needs,
|
|
17
|
+
# You can configure this database to retain all data (persistent) or to expire data according to your rules.
|
|
18
|
+
# If you need to save traces indefinitely, you can download them from Perfetto after opening them there.
|
|
19
|
+
#
|
|
20
|
+
# @example Adding the sampler to your schema
|
|
21
|
+
# class MySchema < GraphQL::Schema
|
|
22
|
+
# # Add the sampler:
|
|
23
|
+
# use GraphQL::Tracing::DetailedTrace, redis: Redis.new(...), limit: 100
|
|
24
|
+
#
|
|
25
|
+
# # And implement this hook to tell it when to take a sample:
|
|
26
|
+
# def self.detailed_trace?(query)
|
|
27
|
+
# # Could use `query.context`, `query.selected_operation_name`, `query.query_string` here
|
|
28
|
+
# # Could call out to Flipper, etc
|
|
29
|
+
# rand <= 0.000_1 # one in ten thousand
|
|
30
|
+
# end
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# @see Graphql::Dashboard GraphQL::Dashboard for viewing stored results
|
|
34
|
+
#
|
|
35
|
+
# @example Customizing debug output in traces
|
|
36
|
+
# class CustomDetailedTrace < GraphQL::Tracing::DetailedTrace
|
|
37
|
+
# def inspect_object(object)
|
|
38
|
+
# if object.is_a?(SomeThing)
|
|
39
|
+
# # handle it specially ...
|
|
40
|
+
# else
|
|
41
|
+
# super
|
|
42
|
+
# end
|
|
43
|
+
# end
|
|
44
|
+
# end
|
|
45
|
+
#
|
|
46
|
+
# @example disabling debug annotations completely
|
|
47
|
+
# use DetailedTrace, debug: false, ...
|
|
48
|
+
#
|
|
49
|
+
# @example disabling debug annotations for one query
|
|
50
|
+
# MySchema.execute(query_str, context: { detailed_trace_debug: false })
|
|
51
|
+
#
|
|
52
|
+
class DetailedTrace
|
|
53
|
+
# @param redis [Redis] If provided, profiles will be stored in Redis for later review
|
|
54
|
+
# @param limit [Integer] A maximum number of profiles to store
|
|
55
|
+
# @param debug [Boolean] if `false`, it won't create `debug` annotations in Perfetto traces (reduces overhead)
|
|
56
|
+
def self.use(schema, trace_mode: :profile_sample, memory: false, debug: debug?, redis: nil, limit: nil)
|
|
57
|
+
storage = if redis
|
|
58
|
+
RedisBackend.new(redis: redis, limit: limit)
|
|
59
|
+
elsif memory
|
|
60
|
+
MemoryBackend.new(limit: limit)
|
|
61
|
+
else
|
|
62
|
+
raise ArgumentError, "Pass `redis: ...` to store traces in Redis for later review"
|
|
63
|
+
end
|
|
64
|
+
detailed_trace = self.new(storage: storage, trace_mode: trace_mode, debug: debug)
|
|
65
|
+
schema.detailed_trace = detailed_trace
|
|
66
|
+
schema.trace_with(PerfettoTrace, mode: trace_mode, save_profile: true)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def initialize(storage:, trace_mode:, debug:)
|
|
70
|
+
@storage = storage
|
|
71
|
+
@trace_mode = trace_mode
|
|
72
|
+
@debug = debug
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @return [Symbol] The trace mode to use when {Schema.detailed_trace?} returns `true`
|
|
76
|
+
attr_reader :trace_mode
|
|
77
|
+
|
|
78
|
+
# @return [String] ID of saved trace
|
|
79
|
+
def save_trace(operation_name, duration_ms, begin_ms, trace_data)
|
|
80
|
+
@storage.save_trace(operation_name, duration_ms, begin_ms, trace_data)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @return [Boolean]
|
|
84
|
+
def debug?
|
|
85
|
+
@debug
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @param last [Integer]
|
|
89
|
+
# @param before [Integer] Timestamp in milliseconds since epoch
|
|
90
|
+
# @return [Enumerable<StoredTrace>]
|
|
91
|
+
def traces(last: nil, before: nil)
|
|
92
|
+
@storage.traces(last: last, before: before)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# @return [StoredTrace, nil]
|
|
96
|
+
def find_trace(id)
|
|
97
|
+
@storage.find_trace(id)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# @return [void]
|
|
101
|
+
def delete_trace(id)
|
|
102
|
+
@storage.delete_trace(id)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# @return [void]
|
|
106
|
+
def delete_all_traces
|
|
107
|
+
@storage.delete_all_traces
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def inspect_object(object)
|
|
111
|
+
self.class.inspect_object(object)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def self.inspect_object(object)
|
|
115
|
+
if defined?(ActiveRecord::Relation) && object.is_a?(ActiveRecord::Relation)
|
|
116
|
+
"#{object.class}, .to_sql=#{object.to_sql.inspect}"
|
|
117
|
+
else
|
|
118
|
+
object.inspect
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Default debug setting
|
|
123
|
+
# @return [true]
|
|
124
|
+
def self.debug?
|
|
125
|
+
true
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
class StoredTrace
|
|
129
|
+
def initialize(id:, operation_name:, duration_ms:, begin_ms:, trace_data:)
|
|
130
|
+
@id = id
|
|
131
|
+
@operation_name = operation_name
|
|
132
|
+
@duration_ms = duration_ms
|
|
133
|
+
@begin_ms = begin_ms
|
|
134
|
+
@trace_data = trace_data
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
attr_reader :id, :operation_name, :duration_ms, :begin_ms, :trace_data
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -1,67 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
module GraphQL
|
|
3
|
-
module Tracing
|
|
4
|
-
# This trace class calls legacy-style tracer with payload hashes.
|
|
5
|
-
# New-style `trace_with` modules significantly reduce the overhead of tracing,
|
|
6
|
-
# but that advantage is lost when legacy-style tracers are also used (since the payload hashes are still constructed).
|
|
7
|
-
module CallLegacyTracers
|
|
8
|
-
def lex(query_string:)
|
|
9
|
-
(@multiplex || @query).trace("lex", { query_string: query_string }) { super }
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def parse(query_string:)
|
|
13
|
-
(@multiplex || @query).trace("parse", { query_string: query_string }) { super }
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def validate(query:, validate:)
|
|
17
|
-
query.trace("validate", { validate: validate, query: query }) { super }
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def analyze_multiplex(multiplex:)
|
|
21
|
-
multiplex.trace("analyze_multiplex", { multiplex: multiplex }) { super }
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def analyze_query(query:)
|
|
25
|
-
query.trace("analyze_query", { query: query }) { super }
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def execute_multiplex(multiplex:)
|
|
29
|
-
multiplex.trace("execute_multiplex", { multiplex: multiplex }) { super }
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def execute_query(query:)
|
|
33
|
-
query.trace("execute_query", { query: query }) { super }
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def execute_query_lazy(query:, multiplex:)
|
|
37
|
-
multiplex.trace("execute_query_lazy", { multiplex: multiplex, query: query }) { super }
|
|
38
|
-
end
|
|
39
2
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def execute_field_lazy(field:, query:, ast_node:, arguments:, object:)
|
|
45
|
-
query.trace("execute_field_lazy", { field: field, query: query, ast_node: ast_node, arguments: arguments, object: object, owner: field.owner, path: query.context[:current_path] }) { super }
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def authorized(query:, type:, object:)
|
|
49
|
-
query.trace("authorized", { context: query.context, type: type, object: object, path: query.context[:current_path] }) { super }
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def authorized_lazy(query:, type:, object:)
|
|
53
|
-
query.trace("authorized_lazy", { context: query.context, type: type, object: object, path: query.context[:current_path] }) { super }
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def resolve_type(query:, type:, object:)
|
|
57
|
-
query.trace("resolve_type", { context: query.context, type: type, object: object, path: query.context[:current_path] }) { super }
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def resolve_type_lazy(query:, type:, object:)
|
|
61
|
-
query.trace("resolve_type_lazy", { context: query.context, type: type, object: object, path: query.context[:current_path] }) { super }
|
|
62
|
-
end
|
|
63
|
-
end
|
|
3
|
+
require "graphql/tracing/trace"
|
|
4
|
+
require "graphql/tracing/call_legacy_tracers"
|
|
64
5
|
|
|
6
|
+
module GraphQL
|
|
7
|
+
module Tracing
|
|
65
8
|
class LegacyTrace < Trace
|
|
66
9
|
include CallLegacyTracers
|
|
67
10
|
end
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GraphQL
|
|
4
|
+
module Tracing
|
|
5
|
+
# This module is the basis for Ruby-level integration with third-party monitoring platforms.
|
|
6
|
+
# Platform-specific traces include this module and implement an adapter.
|
|
7
|
+
#
|
|
8
|
+
# @see ActiveSupportNotificationsTrace Integration via ActiveSupport::Notifications, an alternative approach.
|
|
9
|
+
module MonitorTrace
|
|
10
|
+
class Monitor
|
|
11
|
+
def initialize(trace:, set_transaction_name:, **_rest)
|
|
12
|
+
@trace = trace
|
|
13
|
+
@set_transaction_name = set_transaction_name
|
|
14
|
+
@platform_field_key_cache = Hash.new { |h, k| h[k] = platform_field_key(k) }.compare_by_identity
|
|
15
|
+
@platform_authorized_key_cache = Hash.new { |h, k| h[k] = platform_authorized_key(k) }.compare_by_identity
|
|
16
|
+
@platform_resolve_type_key_cache = Hash.new { |h, k| h[k] = platform_resolve_type_key(k) }.compare_by_identity
|
|
17
|
+
@platform_source_class_key_cache = Hash.new { |h, source_cls| h[source_cls] = platform_source_class_key(source_cls) }.compare_by_identity
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def instrument(keyword, object, &block)
|
|
21
|
+
raise "Implement #{self.class}#instrument to measure the block"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def start_event(keyword, object)
|
|
25
|
+
ev = self.class::Event.new(self, keyword, object)
|
|
26
|
+
ev.start
|
|
27
|
+
ev
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Get the transaction name based on the operation type and name if possible, or fall back to a user provided
|
|
31
|
+
# one. Useful for anonymous queries.
|
|
32
|
+
def transaction_name(query)
|
|
33
|
+
selected_op = query.selected_operation
|
|
34
|
+
txn_name = if selected_op
|
|
35
|
+
op_type = selected_op.operation_type
|
|
36
|
+
op_name = selected_op.name || fallback_transaction_name(query.context) || "anonymous"
|
|
37
|
+
"#{op_type}.#{op_name}"
|
|
38
|
+
else
|
|
39
|
+
"query.anonymous"
|
|
40
|
+
end
|
|
41
|
+
"GraphQL/#{txn_name}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def fallback_transaction_name(context)
|
|
45
|
+
context[:tracing_fallback_transaction_name]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def name_for(keyword, object)
|
|
49
|
+
case keyword
|
|
50
|
+
when :execute_field
|
|
51
|
+
@platform_field_key_cache[object]
|
|
52
|
+
when :authorized
|
|
53
|
+
@platform_authorized_key_cache[object]
|
|
54
|
+
when :resolve_type
|
|
55
|
+
@platform_resolve_type_key_cache[object]
|
|
56
|
+
when :dataloader_source
|
|
57
|
+
@platform_source_class_key_cache[object.class]
|
|
58
|
+
when :parse then self.class::PARSE_NAME
|
|
59
|
+
when :lex then self.class::LEX_NAME
|
|
60
|
+
when :execute then self.class::EXECUTE_NAME
|
|
61
|
+
when :analyze then self.class::ANALYZE_NAME
|
|
62
|
+
when :validate then self.class::VALIDATE_NAME
|
|
63
|
+
else
|
|
64
|
+
raise "No name for #{keyword.inspect}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
class Event
|
|
69
|
+
def initialize(monitor, keyword, object)
|
|
70
|
+
@monitor = monitor
|
|
71
|
+
@keyword = keyword
|
|
72
|
+
@object = object
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
attr_reader :keyword, :object
|
|
76
|
+
|
|
77
|
+
def start
|
|
78
|
+
raise "Implement #{self.class}#start to begin a new event (#{inspect})"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def finish
|
|
82
|
+
raise "Implement #{self.class}#finish to end this event (#{inspect})"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
module GraphQLSuffixNames
|
|
87
|
+
PARSE_NAME = "parse.graphql"
|
|
88
|
+
LEX_NAME = "lex.graphql"
|
|
89
|
+
VALIDATE_NAME = "validate.graphql"
|
|
90
|
+
EXECUTE_NAME = "execute.graphql"
|
|
91
|
+
ANALYZE_NAME = "analyze.graphql"
|
|
92
|
+
|
|
93
|
+
def platform_field_key(field)
|
|
94
|
+
"#{field.path}.graphql"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def platform_authorized_key(type)
|
|
98
|
+
"#{type.graphql_name}.authorized.graphql"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def platform_resolve_type_key(type)
|
|
102
|
+
"#{type.graphql_name}.resolve_type.graphql"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def platform_source_class_key(source_class)
|
|
106
|
+
"#{source_class.name.gsub("::", "_")}.fetch.graphql"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
module GraphQLPrefixNames
|
|
111
|
+
PARSE_NAME = "graphql.parse"
|
|
112
|
+
LEX_NAME = "graphql.lex"
|
|
113
|
+
VALIDATE_NAME = "graphql.validate"
|
|
114
|
+
EXECUTE_NAME = "graphql.execute"
|
|
115
|
+
ANALYZE_NAME = "graphql.analyze"
|
|
116
|
+
|
|
117
|
+
def platform_field_key(field)
|
|
118
|
+
"graphql.#{field.path}"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def platform_authorized_key(type)
|
|
122
|
+
"graphql.authorized.#{type.graphql_name}"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def platform_resolve_type_key(type)
|
|
126
|
+
"graphql.resolve_type.#{type.graphql_name}"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def platform_source_class_key(source_class)
|
|
130
|
+
"graphql.fetch.#{source_class.name.gsub("::", "_")}"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def self.create_module(monitor_name)
|
|
136
|
+
if !monitor_name.match?(/[a-z]+/)
|
|
137
|
+
raise ArgumentError, "monitor name must be [a-z]+, not: #{monitor_name.inspect}"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
trace_module = Module.new
|
|
141
|
+
code = MODULE_TEMPLATE % {
|
|
142
|
+
monitor: monitor_name,
|
|
143
|
+
monitor_class: monitor_name.capitalize + "Monitor",
|
|
144
|
+
}
|
|
145
|
+
trace_module.module_eval(code, __FILE__, __LINE__ + 5) # rubocop:disable Development/NoEvalCop This is build-time with a validated string
|
|
146
|
+
trace_module
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
MODULE_TEMPLATE = <<~RUBY
|
|
150
|
+
# @param set_transaction_name [Boolean] If `true`, use the GraphQL operation name as the request name on the monitoring platform
|
|
151
|
+
# @param trace_scalars [Boolean] If `true`, leaf fields will be traced too (Scalars _and_ Enums)
|
|
152
|
+
# @param trace_authorized [Boolean] If `false`, skip tracing `authorized?` calls
|
|
153
|
+
# @param trace_resolve_type [Boolean] If `false`, skip tracing `resolve_type?` calls
|
|
154
|
+
def initialize(...)
|
|
155
|
+
setup_%{monitor}_monitor(...)
|
|
156
|
+
super
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def setup_%{monitor}_monitor(trace_scalars: false, trace_authorized: true, trace_resolve_type: true, set_transaction_name: false, **kwargs)
|
|
160
|
+
@trace_scalars = trace_scalars
|
|
161
|
+
@trace_authorized = trace_authorized
|
|
162
|
+
@trace_resolve_type = trace_resolve_type
|
|
163
|
+
@set_transaction_name = set_transaction_name
|
|
164
|
+
@%{monitor} = %{monitor_class}.new(trace: self, set_transaction_name: @set_transaction_name, **kwargs)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def parse(query_string:)
|
|
168
|
+
@%{monitor}.instrument(:parse, query_string) do
|
|
169
|
+
super
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def lex(query_string:)
|
|
174
|
+
@%{monitor}.instrument(:lex, query_string) do
|
|
175
|
+
super
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def validate(query:, validate:)
|
|
180
|
+
@%{monitor}.instrument(:validate, query) do
|
|
181
|
+
super
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def begin_analyze_multiplex(multiplex, analyzers)
|
|
186
|
+
begin_%{monitor}_event(:analyze, nil)
|
|
187
|
+
super
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def end_analyze_multiplex(multiplex, analyzers)
|
|
191
|
+
finish_%{monitor}_event
|
|
192
|
+
super
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def execute_multiplex(multiplex:)
|
|
196
|
+
@%{monitor}.instrument(:execute, multiplex) do
|
|
197
|
+
super
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def begin_execute_field(field, object, arguments, query)
|
|
202
|
+
return_type = field.type.unwrap
|
|
203
|
+
trace_field = if return_type.kind.scalar? || return_type.kind.enum?
|
|
204
|
+
(field.trace.nil? && @trace_scalars) || field.trace
|
|
205
|
+
else
|
|
206
|
+
true
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
if trace_field
|
|
210
|
+
begin_%{monitor}_event(:execute_field, field)
|
|
211
|
+
end
|
|
212
|
+
super
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def end_execute_field(field, object, arguments, query, result)
|
|
216
|
+
finish_%{monitor}_event
|
|
217
|
+
super
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def dataloader_fiber_yield(source)
|
|
221
|
+
Fiber[PREVIOUS_EV_KEY] = finish_%{monitor}_event
|
|
222
|
+
super
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def dataloader_fiber_resume(source)
|
|
226
|
+
prev_ev = Fiber[PREVIOUS_EV_KEY]
|
|
227
|
+
if prev_ev
|
|
228
|
+
begin_%{monitor}_event(prev_ev.keyword, prev_ev.object)
|
|
229
|
+
end
|
|
230
|
+
super
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def begin_authorized(type, object, context)
|
|
234
|
+
@trace_authorized && begin_%{monitor}_event(:authorized, type)
|
|
235
|
+
super
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def end_authorized(type, object, context, result)
|
|
239
|
+
finish_%{monitor}_event
|
|
240
|
+
super
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def begin_resolve_type(type, value, context)
|
|
244
|
+
@trace_resolve_type && begin_%{monitor}_event(:resolve_type, type)
|
|
245
|
+
super
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def end_resolve_type(type, value, context, resolved_type)
|
|
249
|
+
finish_%{monitor}_event
|
|
250
|
+
super
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def begin_dataloader_source(source)
|
|
254
|
+
begin_%{monitor}_event(:dataloader_source, source)
|
|
255
|
+
super
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def end_dataloader_source(source)
|
|
259
|
+
finish_%{monitor}_event
|
|
260
|
+
super
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
CURRENT_EV_KEY = :__graphql_%{monitor}_trace_event
|
|
264
|
+
PREVIOUS_EV_KEY = :__graphql_%{monitor}_trace_previous_event
|
|
265
|
+
|
|
266
|
+
private
|
|
267
|
+
|
|
268
|
+
def begin_%{monitor}_event(keyword, object)
|
|
269
|
+
Fiber[CURRENT_EV_KEY] = @%{monitor}.start_event(keyword, object)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def finish_%{monitor}_event
|
|
273
|
+
if ev = Fiber[CURRENT_EV_KEY]
|
|
274
|
+
ev.finish
|
|
275
|
+
# Use `false` to prevent grabbing an event from a parent fiber
|
|
276
|
+
Fiber[CURRENT_EV_KEY] = false
|
|
277
|
+
ev
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
RUBY
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|