Skip to content

Commit 8464143

Browse files
authored
Merge pull request #1126 from gavin-dunlap-luminar/bugfix/intermittent-cache-clear-bug
fix: Correct race conditions
2 parents 77ec4d9 + 44c90b0 commit 8464143

File tree

2 files changed

+74
-58
lines changed

2 files changed

+74
-58
lines changed

lib/ceedling/file_path_collection_utils.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,9 @@ def revise_filelist(list, revisions)
121121
paths = (plus - minus).to_a
122122
paths.map! {|path| shortest_path_from_working(path) }
123123

124-
return FileList.new( paths )
124+
result = FileList.new( paths )
125+
result.resolve() # Force expansion to prevent race conditions in threaded context
126+
return result
125127
end
126128

127129
def shortest_path_from_working(path)

lib/ceedling/preprocessinator.rb

Lines changed: 71 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -26,64 +26,78 @@ def setup
2626
# Aliases
2727
@includes_handler = @preprocessinator_includes_handler
2828
@file_handler = @preprocessinator_file_handler
29+
30+
# Thread-safe per-file locking for YAML cache operations
31+
# Key: includes_list_filepath (String), Value: Mutex
32+
@file_locks = {}
33+
@file_locks_mutex = Mutex.new
2934
end
3035

3136

3237
def preprocess_includes(filepath:, test:, flags:, include_paths:, defines:, deep: false)
3338
includes_list_filepath = @file_path_utils.form_preprocessed_includes_list_filepath( filepath, test )
3439

35-
includes = []
40+
# Get or create a mutex for this specific cache file
41+
file_lock = @file_locks_mutex.synchronize do
42+
@file_locks[includes_list_filepath] ||= Mutex.new
43+
end
3644

37-
# If existing YAML file of includes is newer than the file we're processing, skip preprocessing
38-
if @file_wrapper.newer?( includes_list_filepath, filepath )
39-
msg = @reportinator.generate_module_progress(
40-
operation: "Loading #include statement listing file for",
41-
module_name: test,
42-
filename: File.basename(filepath)
43-
)
44-
@loginator.log( msg, Verbosity::NORMAL )
45-
46-
# Note: It's possible empty YAML content returns nil
47-
includes = @yaml_wrapper.load( includes_list_filepath )
48-
49-
msg = "Loaded existing #include list from #{includes_list_filepath}:"
50-
51-
if includes.nil? or includes.empty?
52-
# Ensure includes defaults to emtpy array to prevent external iteration problems
53-
includes = []
54-
msg += ' <empty>'
55-
else
56-
includes.each { |include| msg += "\n - #{include}" }
57-
end
45+
includes = []
5846

59-
@loginator.log( msg, Verbosity::DEBUG )
60-
@loginator.log( '', Verbosity::DEBUG )
61-
62-
# Full preprocessing-based #include extraction with saving to YAML file
63-
else
64-
includes = @includes_handler.extract_includes(
65-
filepath: filepath,
66-
test: test,
67-
flags: flags,
68-
include_paths: include_paths,
69-
defines: defines,
70-
deep: deep
71-
)
72-
73-
msg = "Extracted #include list from #{filepath}:"
74-
75-
if includes.nil? or includes.empty?
76-
# Ensure includes defaults to emtpy array to prevent external iteration problems
77-
includes = []
78-
msg += ' <empty>'
47+
# Wrap the entire check-read-or-extract-write operation in a mutex
48+
# This prevents race conditions when multiple threads process the same file
49+
file_lock.synchronize do
50+
# If existing YAML file of includes is newer than the file we're processing, skip preprocessing
51+
if @file_wrapper.newer?( includes_list_filepath, filepath )
52+
msg = @reportinator.generate_module_progress(
53+
operation: "Loading #include statement listing file for",
54+
module_name: test,
55+
filename: File.basename(filepath)
56+
)
57+
@loginator.log( msg, Verbosity::NORMAL )
58+
59+
# Note: It's possible empty YAML content returns nil
60+
includes = @yaml_wrapper.load( includes_list_filepath )
61+
62+
msg = "Loaded existing #include list from #{includes_list_filepath}:"
63+
64+
if includes.nil? or includes.empty?
65+
# Ensure includes defaults to emtpy array to prevent external iteration problems
66+
includes = []
67+
msg += ' <empty>'
68+
else
69+
includes.each { |include| msg += "\n - #{include}" }
70+
end
71+
72+
@loginator.log( msg, Verbosity::DEBUG )
73+
@loginator.log( '', Verbosity::DEBUG )
74+
75+
# Full preprocessing-based #include extraction with saving to YAML file
7976
else
80-
includes.each { |include| msg += "\n - #{include}" }
77+
includes = @includes_handler.extract_includes(
78+
filepath: filepath,
79+
test: test,
80+
flags: flags,
81+
include_paths: include_paths,
82+
defines: defines,
83+
deep: deep
84+
)
85+
86+
msg = "Extracted #include list from #{filepath}:"
87+
88+
if includes.nil? or includes.empty?
89+
# Ensure includes defaults to emtpy array to prevent external iteration problems
90+
includes = []
91+
msg += ' <empty>'
92+
else
93+
includes.each { |include| msg += "\n - #{include}" }
94+
end
95+
96+
@loginator.log( msg, Verbosity::DEBUG )
97+
@loginator.log( '', Verbosity::DEBUG )
98+
99+
@includes_handler.write_includes_list( includes_list_filepath, includes )
81100
end
82-
83-
@loginator.log( msg, Verbosity::DEBUG )
84-
@loginator.log( '', Verbosity::DEBUG )
85-
86-
@includes_handler.write_includes_list( includes_list_filepath, includes )
87101
end
88102

89103
return includes
@@ -102,7 +116,7 @@ def preprocess_mockable_header_file(filepath:, test:, flags:, include_paths:, de
102116
test: test,
103117
flags: flags,
104118
include_paths: include_paths,
105-
defines: defines
119+
defines: defines
106120
}
107121

108122
# Trigger pre_mock_preprocessing plugin hook
@@ -114,10 +128,10 @@ def preprocess_mockable_header_file(filepath:, test:, flags:, include_paths:, de
114128
flags: flags,
115129
include_paths: include_paths,
116130
defines: defines,
117-
deep: preprocess_deep
131+
deep: preprocess_deep
118132
}
119133

120-
# Extract includes & log progress and details
134+
# Extract includes & log progress and details
121135
includes = preprocess_file_common( **arg_hash )
122136

123137
arg_hash = {
@@ -140,7 +154,7 @@ def preprocess_mockable_header_file(filepath:, test:, flags:, include_paths:, de
140154
preprocessed_filepath: preprocessed_filepath,
141155
contents: contents,
142156
extras: extras,
143-
includes: includes
157+
includes: includes
144158
}
145159

146160
# Create a reconstituted header file from preprocessing expansion and preserving any extras
@@ -165,7 +179,7 @@ def preprocess_test_file(filepath:, test:, flags:, include_paths:, defines:)
165179
test: test,
166180
flags: flags,
167181
include_paths: include_paths,
168-
defines: defines
182+
defines: defines
169183
}
170184

171185
# Trigger pre_test_preprocess plugin hook
@@ -177,7 +191,7 @@ def preprocess_test_file(filepath:, test:, flags:, include_paths:, defines:)
177191
flags: flags,
178192
include_paths: include_paths,
179193
defines: defines,
180-
deep: preprocess_deep
194+
deep: preprocess_deep
181195
}
182196

183197
# Extract includes & log progress and info
@@ -188,7 +202,7 @@ def preprocess_test_file(filepath:, test:, flags:, include_paths:, defines:)
188202
test: test,
189203
flags: flags,
190204
include_paths: include_paths,
191-
defines: defines
205+
defines: defines
192206
}
193207

194208
# `contents` & `extras` are arrays of text strings to be assembled in generating a new test file.
@@ -200,7 +214,7 @@ def preprocess_test_file(filepath:, test:, flags:, include_paths:, defines:)
200214
preprocessed_filepath: preprocessed_filepath,
201215
contents: contents,
202216
extras: extras,
203-
includes: includes
217+
includes: includes
204218
}
205219

206220
# Create a reconstituted test file from preprocessing expansion and preserving any extras
@@ -231,7 +245,7 @@ def preprocess_file_common(filepath:, test:, flags:, include_paths:, defines:, d
231245
flags: flags,
232246
include_paths: include_paths,
233247
defines: defines,
234-
deep: deep)
248+
deep: deep)
235249

236250
return includes
237251
end

0 commit comments

Comments
 (0)