Skip to content

Commit b5afbcd

Browse files
committed
Never lose a run to a failed rsync
Testing pull request 16696 finished its 96 libraries and then died publishing them, taking three hours of results with it. Two separate problems. **The rsync failure.** `publishLibrary` created the remote directories from four joblib threads, each running `rsync -aR emptydir/./<lib>/files <branch>`. rsync's receiver creates the destination directory with a plain mkdir, and the destination here was `branches/pr/16696`, shared by all four: three won, `Modelica_3.2.3` lost and got rsync: [Receiver] mkdir ".../pr/16696" failed: File exists (17) rsync error: error in file IO (code 11) A branch tested before has that directory already, so only the per-library components are created and they never collide -- this is a pull request's first run every time, and `branches/pr/` existing from an earlier one does not help, `16696` itself is the contended level. The `mkdir -p` the Jenkinsfile does looks like it covers this but runs on the build node, not on the web server. Create the directories once, serially, before the threads start, and with `--mkpath` so the mkdir tolerates a directory another rsync made and so the two levels of a `pr/<number>` are created at all. That also drops one ssh connection per library. **The lost results.** The transaction was held open until after publishing, so the exception rolled back everything the run had measured. Commit before generating and publishing the reports instead: rows whose pages are missing are a report to publish again, rather than a run to do again. `clean-empty-omcversion-dates.py` still drops a date that got no rows. A library that cannot be published no longer aborts the other 95 either; the failures are collected and the run fails once at the end. Checked with `configs/sanityCheck.json` and a stub rsync: one `-aR --mkpath <lib1>/files <lib2>/files <dest>` followed by one content rsync per library, and with a failure injected into one library the other still publishes, all rows reach the database and the exit status is 1. Assisted-by: Claude Opus 5 (1M context)
1 parent 1418d01 commit b5afbcd

1 file changed

Lines changed: 40 additions & 12 deletions

File tree

test.py

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
if (sys.version_info < (3, 0)):
99
raise Exception("Python2 is no longer supported")
1010

11-
import html, shutil, os, re, glob, time, argparse, datetime, math, platform
11+
import html, shutil, os, re, glob, time, argparse, datetime, math, platform, traceback
1212
from joblib import Parallel, delayed
1313
import simplejson as json
1414
import psutil, subprocess, threading, hashlib
@@ -1259,6 +1259,10 @@ def cpu_name():
12591259
confighash = stats_by_libname[libname]["conf"]["confighash"]
12601260
cursor.execute("INSERT INTO libversion VALUES (?,?,?,?,?,?,?)%s" % db.insertIgnore(), (testRunStartTimeAsEpoch, resultBranch, libname, stats_by_libname[libname]["conf"]["libraryLastChange"], confighash, hostname, sysInfo))
12611261
cursor.execute("INSERT INTO omcversion VALUES (?,?,?)%s" % db.insertIgnore(), (testRunStartTimeAsEpoch, resultBranch, omc_version))
1262+
1263+
db.commit()
1264+
db.release()
1265+
12621266
"""
12631267
# Not really a good thing to do; was just done to make generation of the report simpler
12641268
for libname in skipped_libs.keys():
@@ -1425,6 +1429,7 @@ def referenceFilesVersion(c):
14251429
# Every runner publishes its own results - .sim and diff files included - to the
14261430
# directory of its own branch; the .err of the build is shared, so each of them
14271431
# gets a copy of it.
1432+
publishFailures = []
14281433
for (resultBranch, runner) in resultBranches:
14291434
result_location = outputFor(resultBranch)
14301435
if result_location != "" and (isWin or noSync):
@@ -1435,6 +1440,20 @@ def referenceFilesVersion(c):
14351440
rmtree(resRootPath)
14361441
os.makedirs(resRootPath)
14371442

1443+
def makeRemoteDirs(libnames):
1444+
"""Create the branch directory and every library's files/ in one connection."""
1445+
byStage = {}
1446+
for libname in libnames:
1447+
simulator = simulatorKey(libname, runner)
1448+
stageRoot = stageRootFor(simulator, artifactSuffix(simulator))
1449+
os.makedirs(os.path.join(stageRoot, "emptydir", libname, "files"), exist_ok=True)
1450+
byStage.setdefault(stageRoot, []).append(libname)
1451+
for (stageRoot, libs) in byStage.items():
1452+
# The /./ tells rsync -R where the relative part starts.
1453+
check_output_log(["rsync", "-aR", "--mkpath"]
1454+
+ ["emptydir/./%s/files" % l for l in libs]
1455+
+ [result_location], cwd=stageRoot)
1456+
14381457
htmltpl=open("library.html.tpl").read()
14391458
def publishLibrary(libname):
14401459
if libname in skipped_libs or not ranRunner(libname, runner):
@@ -1542,16 +1561,10 @@ def publishLibrary(libname):
15421561
# move results by sync operations (not available under win)
15431562
if result_location != "" and not isWin and not noSync:
15441563
result_location_libname = "%s/%s" % (result_location, libname)
1545-
def makeRemoteDirs():
1546-
# The /./ tells rsync -R where the relative part starts, so the branch,
1547-
# the library and its files directory are created in one connection.
1548-
os.makedirs(os.path.join(stageRoot, "emptydir", libname, "files"), exist_ok=True)
1549-
check_output_log(["rsync", "-aR", "emptydir/./%s/files" % libname, result_location], cwd=stageRoot)
1550-
makeRemoteDirs()
15511564
try:
15521565
check_output_log(["rsync", "-aR", "--delete-excluded", "--include-from=%s.files" % libname, "--exclude=*", "./", result_location_libname], cwd=stageRoot)
15531566
except:
1554-
makeRemoteDirs()
1567+
makeRemoteDirs([libname])
15551568
check_output_log(["rsync", "-aR", "--delete-excluded", "--include-from=%s.files" % libname, "--exclude=*", "./", result_location_libname], cwd=stageRoot)
15561569
if (conf.get("referenceFiles") or "") != "" and dygraphs:
15571570
check_output_log(["rsync", "-a", dygraphs, result_location_libname+"/files"])
@@ -1588,7 +1601,21 @@ def makeRemoteDirs():
15881601
print("-- problem during file copy... maybe the file is still hooked by a process... :" + file)
15891602
pass
15901603

1591-
Parallel(n_jobs=PUBLISH_JOBS, backend="threading")(delayed(publishLibrary)(libname) for libname in stats_by_libname.keys())
1604+
publishable = [l for l in stats_by_libname.keys() if l not in skipped_libs and ranRunner(l, runner)]
1605+
if result_location != "" and not isWin and not noSync:
1606+
makeRemoteDirs(publishable)
1607+
1608+
def publishLibraryReportingFailure(libname):
1609+
try:
1610+
publishLibrary(libname)
1611+
except Exception:
1612+
print("Failed to publish %s of %s:" % (libname, resultBranch))
1613+
traceback.print_exc()
1614+
sys.stdout.flush()
1615+
return "%s/%s" % (resultBranch, libname)
1616+
1617+
publishFailures += [f for f in Parallel(n_jobs=PUBLISH_JOBS, backend="threading")
1618+
(delayed(publishLibraryReportingFailure)(libname) for libname in publishable) if f]
15921619

15931620
if clean:
15941621
for g in ["*.o","*.so","*.h","*.c","*.cpp","*.simsuccess","*.conf.json","*.tmpfiles","*.log","*.libs","OMCpp*","*.fmu*","temp_*", "*.exe", "HelloWorld.bat", "*.makefile", "*.mat","*.xml", "*.bin", "*.json"]:
@@ -1608,9 +1635,10 @@ def makeRemoteDirs():
16081635
except:
16091636
print("-- problem during removing of ./files dir")
16101637

1611-
# Do not commit until we have generated and uploaded the reports
1612-
db.commit()
1613-
db.release()
16141638
db.close()
16151639

1640+
if publishFailures:
1641+
print("Failed to publish: %s" % ", ".join(publishFailures))
1642+
sys.exit(1)
1643+
16161644
print("all tests done ...")

0 commit comments

Comments
 (0)