Skip to content

Commit 95161ed

Browse files
authored
Merge pull request #593 from DataDog/anmarchenko/isolate-parallel-test-state
fix: isolate state between parallel_tests sessions
2 parents 1cb1c3a + 68dc718 commit 95161ed

13 files changed

Lines changed: 827 additions & 35 deletions

File tree

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ The native extension is in `ext/` directory and compiled for each Ruby version.
106106

107107
- We use Ruby 2.7 syntax
108108
- Use exceptions for exceptional cases, not for control flow.
109+
- Instrumentation must never raise its own errors into customer test processes. Log internal failures at warn or error level and degrade gracefully instead.
109110
- Implement proper error logging and user-friendly messages.
110111
- Do not use `instance_variable_set` or `instance_variable_get`; add explicit APIs instead.
111112

@@ -130,6 +131,7 @@ The native extension is in `ext/` directory and compiled for each Ruby version.
130131

131132
- All changes should be covered by corresponding tests
132133
- Place tests in the `spec/datadog/ci` folder
134+
- Never monkey-patch production code in tests. Do not use techniques such as `prepend`, reopening production classes or modules, or replacing production singleton methods; exercise production code through its normal interfaces instead.
133135
- Do not use `instance_variable_set` or `instance_variable_get` in tests; use mocking or explicit APIs when needed
134136
- Do not make changes to `release_gem_spec.rb` if not asked
135137
- Do not use focused tests feature (fit, fdescribe)

lib/datadog/ci/contrib/parallel_tests/cli.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require_relative "../../ext/test"
4+
require_relative "../../utils/file_storage"
45
require_relative "../rspec/ext"
56

67
module Datadog
@@ -17,7 +18,7 @@ def run_tests_in_parallel(num_processes, options)
1718
# only rspec runner is supported for now
1819
return super if @runner != ::ParallelTests::RSpec::Runner
1920

20-
begin
21+
Utils::FileStorage.with_new_namespace do |storage_namespace|
2122
# Preserve activation explicitly for the RSpec worker commands.
2223
# Starting the distributed parent session removes inherited
2324
# activation before unrelated child processes can receive it.
@@ -35,6 +36,7 @@ def run_tests_in_parallel(num_processes, options)
3536

3637
options[:env] ||= {}
3738
options[:env][CI::Ext::Settings::ENV_TEST_VISIBILITY_DRB_SERVER_URI] = test_tracing_component.context_service_uri
39+
options[:env][Utils::FileStorage::ENV_NAMESPACE] = storage_namespace
3840
options[:env]["RUBYOPT"] ||= worker_rubyopt if worker_rubyopt
3941

4042
super

lib/datadog/ci/test_session.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ module CI
1414
class TestSession < ConcurrentSpan
1515
attr_accessor :estimated_total_tests_count, :distributed
1616

17+
def initialize(tracer_span)
18+
super
19+
20+
@distributed = false
21+
end
22+
1723
# Finishes the current test session.
1824
# @return [void]
1925
def finish

lib/datadog/ci/test_tracing/component.rb

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def configure(library_configuration, test_session)
8282
store_component_state if test_session.distributed
8383
end
8484

85-
def start_test_session(service: nil, tags: {}, estimated_total_tests_count: 0, distributed: false, local_test_suites_mode: true)
85+
def start_test_session(service: nil, tags: {}, estimated_total_tests_count: 0, distributed: nil, local_test_suites_mode: true)
8686
return skip_tracing unless test_suite_level_visibility_enabled
8787

8888
@local_test_suites_mode = local_test_suites_mode
@@ -91,7 +91,7 @@ def start_test_session(service: nil, tags: {}, estimated_total_tests_count: 0, d
9191

9292
test_session = maybe_remote_context.start_test_session(service: service, tags: tags)
9393
test_session.estimated_total_tests_count = estimated_total_tests_count
94-
test_session.distributed = distributed
94+
test_session.distributed = distributed unless distributed.nil?
9595

9696
on_test_session_started(test_session)
9797

@@ -321,8 +321,6 @@ def on_test_session_finished(test_session)
321321
DeprecatedTotalCoverageMetric.extract_lines_pct(test_session)
322322

323323
Telemetry.event_finished(test_session)
324-
325-
Utils::FileStorage.cleanup
326324
end
327325

328326
def on_test_module_finished(test_module)

lib/datadog/ci/test_tracing/null_component.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ def configure(_, _)
99
end
1010

1111
def start_test_session(
12-
service: nil, tags: {}, estimated_total_tests_count: 0, distributed: false, local_test_suites_mode: true
12+
service: nil, tags: {}, estimated_total_tests_count: 0, distributed: nil, local_test_suites_mode: true
1313
)
1414
skip_tracing
1515
end

lib/datadog/ci/utils/file_storage.rb

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,39 @@
11
# frozen_string_literal: true
22

33
require "fileutils"
4-
require "tempfile"
4+
require "securerandom"
5+
require "tmpdir"
56

67
module Datadog
78
module CI
89
module Utils
910
# FileStorage module provides functionality for storing and retrieving arbitrary Ruby objects in a temp file
1011
# to share them between processes.
1112
module FileStorage
13+
class MissingNamespaceError < StandardError; end
14+
1215
TEMP_DIR = File.join(Dir.tmpdir, "datadog-ci-storage")
16+
ENV_NAMESPACE = "DD_CIVISIBILITY_PARALLEL_TESTS_RUN_ID"
1317

1418
def self.store(key, value)
1519
ensure_temp_dir_exists
1620
file_path = file_path_for(key)
21+
temporary_path = File.join(storage_dir, "dd-ci-#{SecureRandom.uuid}.tmp")
1722

18-
File.binwrite(file_path, Marshal.dump(value))
23+
File.open(temporary_path, File::WRONLY | File::CREAT | File::EXCL, 0o600) do |file|
24+
file.binmode
25+
file.write(Marshal.dump(value))
26+
file.flush
27+
file.fsync
28+
end
29+
File.rename(temporary_path, file_path)
1930

2031
true
2132
rescue => e
22-
Datadog.logger.error("Failed to store data for key '#{key}': #{e.class} - #{e.message}")
33+
Datadog.logger.error("Failed to store data for key '#{key}': #{e.class}")
2334
false
35+
ensure
36+
FileUtils.rm_f(temporary_path) if temporary_path
2437
end
2538

2639
def self.retrieve(key)
@@ -29,27 +42,54 @@ def self.retrieve(key)
2942

3043
Marshal.load(File.binread(file_path))
3144
rescue => e
32-
Datadog.logger.error("Failed to retrieve data for key '#{key}': #{e.class} - #{e.message}")
45+
Datadog.logger.error("Failed to retrieve data for key '#{key}': #{e.class}")
3346
nil
3447
end
3548

36-
def self.cleanup
37-
return false unless Dir.exist?(TEMP_DIR)
49+
def self.cleanup(namespace)
50+
directory = storage_dir(namespace)
51+
return false unless Dir.exist?(directory)
3852

39-
FileUtils.rm_rf(TEMP_DIR)
53+
FileUtils.rm_rf(directory)
4054
true
4155
rescue => e
42-
Datadog.logger.error("Failed to cleanup storage directory: #{e.class} - #{e.message}")
56+
Datadog.logger.error("Failed to cleanup storage directory: #{e.class}")
4357
false
4458
end
59+
private_class_method :cleanup
60+
61+
def self.with_new_namespace
62+
previous_namespace = ENV[ENV_NAMESPACE]
63+
namespace = SecureRandom.uuid
64+
ENV[ENV_NAMESPACE] = namespace
65+
66+
yield namespace
67+
ensure
68+
cleanup(namespace) if namespace
69+
70+
if previous_namespace
71+
ENV[ENV_NAMESPACE] = previous_namespace
72+
else
73+
ENV.delete(ENV_NAMESPACE)
74+
end
75+
end
4576

4677
def self.ensure_temp_dir_exists
47-
FileUtils.mkdir_p(TEMP_DIR) unless Dir.exist?(TEMP_DIR)
78+
FileUtils.mkdir_p(storage_dir)
4879
end
4980

5081
def self.file_path_for(key)
5182
sanitized_key = key.to_s.gsub(/[^a-zA-Z0-9_-]/, "_")
52-
File.join(TEMP_DIR, "dd-ci-#{sanitized_key}.dat")
83+
File.join(storage_dir, "dd-ci-#{sanitized_key}.dat")
84+
end
85+
86+
def self.storage_dir(namespace = ENV[ENV_NAMESPACE])
87+
if namespace.nil? || namespace.empty?
88+
raise MissingNamespaceError, "File storage namespace is not set"
89+
end
90+
91+
sanitized_namespace = namespace.gsub(/[^a-zA-Z0-9_-]/, "_")
92+
File.join(TEMP_DIR, sanitized_namespace)
5393
end
5494
end
5595
end

sig/datadog/ci/test_tracing/component.rbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ module Datadog
4343

4444
def trace: (String span_name, ?type: String, ?tags: Hash[Symbol | String, untyped]) ?{ (Datadog::CI::Span span) -> untyped } -> untyped
4545

46-
def start_test_session: (?service: String?, ?tags: Hash[Symbol | String, untyped], ?estimated_total_tests_count: Integer, ?distributed: bool, ?local_test_suites_mode: bool) -> Datadog::CI::TestSession
46+
def start_test_session: (?service: String?, ?tags: Hash[Symbol | String, untyped], ?estimated_total_tests_count: Integer, ?distributed: bool?, ?local_test_suites_mode: bool) -> Datadog::CI::TestSession
4747

4848
def start_test_module: (String test_module_name, ?service: String?, ?tags: Hash[Symbol | String, untyped]) -> Datadog::CI::TestModule
4949

sig/datadog/ci/test_tracing/null_component.rbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ module Datadog
1111
def trace: (String span_name, ?type: String, ?tags: Hash[untyped, untyped]) ?{ (Datadog::CI::Span span) -> untyped } -> untyped
1212

1313
def start_test_session: (
14-
?service: String?, ?tags: Hash[untyped, untyped], ?estimated_total_tests_count: Integer, ?distributed: bool, ?local_test_suites_mode: bool
14+
?service: String?, ?tags: Hash[untyped, untyped], ?estimated_total_tests_count: Integer, ?distributed: bool?, ?local_test_suites_mode: bool
1515
) -> nil
1616

1717
def start_test_module: (String test_module_name, ?service: String?, ?tags: Hash[untyped, untyped]) -> nil

sig/datadog/ci/utils/file_storage.rbs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,27 @@ module Datadog
22
module CI
33
module Utils
44
module FileStorage
5+
class MissingNamespaceError < StandardError
6+
end
7+
58
TEMP_DIR: String
9+
ENV_NAMESPACE: String
610

711
def self.store: (String key, Hash[Symbol, untyped] value) -> bool
812

913
def self.retrieve: (String key) -> Hash[Symbol, untyped]?
1014

11-
def self.cleanup: () -> bool
15+
def self.with_new_namespace: [T] () { (String) -> T } -> T
1216

1317
def self.ensure_temp_dir_exists: () -> void
1418

1519
def self.file_path_for: (String key) -> String
20+
21+
def self.storage_dir: (?String namespace) -> String
22+
23+
private
24+
25+
def self.cleanup: (String namespace) -> bool
1626
end
1727
end
1828
end

0 commit comments

Comments
 (0)