Skip to content

Commit ba6fecc

Browse files
committed
Troubleshoot hanging rspec runs that caused job time outs
Signed-off-by: Jimmy Tanagra <jcode@tanagra.id.au>
1 parent 44e1585 commit ba6fecc

2 files changed

Lines changed: 66 additions & 7 deletions

File tree

.github/workflows/ci.yml

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,14 +163,40 @@ jobs:
163163
key: openHAB-setup-2-${{ matrix.openhab_version }}${{ endsWith(matrix.openhab_version, 'SNAPSHOT') && needs.openhab-snapshot-date.outputs.date || '' }}-java-${{ matrix.java_version }}
164164
fail-on-cache-miss: true
165165
- name: RSpec
166-
run: bin/rspec --format progress --format html --out rspec.html
166+
run: |
167+
set -euo pipefail
168+
bin/rspec --format progress --format html --out rspec.html &
169+
rspec_pid=$!
170+
start_time=$(date +%s)
171+
172+
while kill -0 "$rspec_pid" 2>/dev/null; do
173+
sleep 30
174+
now=$(date +%s)
175+
elapsed=$((now - start_time))
176+
echo "[heartbeat] $(date -u +%FT%TZ) rspec still running (${elapsed}s elapsed)"
177+
178+
if [[ "$elapsed" -ge 330 ]]; then
179+
echo "RSpec exceeded 330s; collecting thread dump before termination"
180+
kill -QUIT "$rspec_pid" || true
181+
sleep 5
182+
kill -TERM "$rspec_pid" || true
183+
sleep 20
184+
kill -KILL "$rspec_pid" || true
185+
wait "$rspec_pid" || true
186+
exit 124
187+
fi
188+
done
189+
190+
wait "$rspec_pid"
167191
timeout-minutes: 6
168192
- name: Upload openHAB Logs
169193
uses: actions/upload-artifact@v6
170194
if: failure()
171195
with:
172196
name: RSpec-logs-${{ matrix.openhab_version }}-${{ matrix.jruby_version }}-java-${{ matrix.java_version }}
173-
path: tmp/openhab/userdata/logs
197+
path: |
198+
tmp/openhab/userdata/logs
199+
tmp/karaf.log
174200
retention-days: 2
175201
- name: Upload RSpec results
176202
uses: actions/upload-artifact@v6

lib/openhab/rspec/karaf.rb

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ def set_up_bundle_listener
382382
@thing_type_tracker.class.field_reader :openState
383383
org.openhab.core.config.core.xml.osgi.XmlDocumentBundleTracker::OpenState.field_reader :OPENED
384384
opened = org.openhab.core.config.core.xml.osgi.XmlDocumentBundleTracker::OpenState.OPENED
385-
sleep until @thing_type_tracker.openState == opened
385+
wait_until("thing type tracker open") { @thing_type_tracker.openState == opened }
386386
@bundle_context.bundles.each do |bundle|
387387
@thing_type_tracker.adding_bundle(bundle, nil)
388388
end
@@ -394,7 +394,7 @@ def set_up_bundle_listener
394394
@config_description_tracker.class.field_reader :openState
395395
org.openhab.core.config.core.xml.osgi.XmlDocumentBundleTracker::OpenState.field_reader :OPENED
396396
opened = org.openhab.core.config.core.xml.osgi.XmlDocumentBundleTracker::OpenState.OPENED
397-
sleep until @config_description_tracker.openState == opened
397+
wait_until("config description tracker open") { @config_description_tracker.openState == opened }
398398
@bundle_context.bundles.each do |bundle|
399399
@config_description_tracker.adding_bundle(bundle, nil)
400400
end
@@ -532,7 +532,7 @@ def wait_for_service(service_name, filter: nil, &block)
532532
end
533533

534534
def wait_for_start
535-
wait do |continue|
535+
wait(timeout: 120, label: "all bundles to start") do |continue|
536536
@all_bundles_continue = continue
537537
next continue.call if all_bundles_started?
538538
end
@@ -557,10 +557,11 @@ def blocked_bundle?(bundle)
557557
bundle.fragment?
558558
end
559559

560-
def wait(timeout: 30)
560+
def wait(timeout: 30, label: nil)
561561
mutex = Mutex.new
562562
cond = ConditionVariable.new
563563
skip_wait = false
564+
timed_out = false
564565

565566
continue = lambda do
566567
# if continue was called synchronously, we can just return
@@ -570,8 +571,40 @@ def wait(timeout: 30)
570571
end
571572
mutex.synchronize do
572573
yield continue
573-
cond.wait(mutex, timeout) unless skip_wait
574+
timed_out = !cond.wait(mutex, timeout) unless skip_wait
574575
end
576+
577+
return unless timed_out
578+
579+
detail = label ? " while waiting for #{label}" : ""
580+
raise "Timed out after #{timeout}s#{detail}. #{bundle_state_summary}"
581+
end
582+
583+
def wait_until(label, timeout: 30, interval: 0.1)
584+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
585+
until yield
586+
if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
587+
raise "Timed out after #{timeout}s while waiting for #{label}. #{bundle_state_summary}"
588+
end
589+
590+
sleep interval
591+
end
592+
end
593+
594+
def bundle_state_summary
595+
return "Bundle state unavailable" unless @bundle_context
596+
597+
bundle_counts = @bundle_context.bundles.each_with_object(Hash.new(0)) do |bundle, counts|
598+
counts[bundle.state] += 1
599+
end
600+
601+
active = org.osgi.framework.Bundle::ACTIVE
602+
resolved = org.osgi.framework.Bundle::RESOLVED
603+
starting = org.osgi.framework.Bundle::STARTING
604+
installed = org.osgi.framework.Bundle::INSTALLED
605+
606+
"Bundles: active=#{bundle_counts[active]}, resolved=#{bundle_counts[resolved]}, " \
607+
"starting=#{bundle_counts[starting]}, installed=#{bundle_counts[installed]}, total=#{@bundle_context.bundles.length}"
575608
end
576609

577610
def link_osgi

0 commit comments

Comments
 (0)