[education/rkward] /: Fix lockup condition while cancelling commands (expecting to pass all tests, on Windows, again)

Thomas Friedrichsmeier null at kde.org
Sun Oct 2 12:33:19 BST 2022


Git commit 2b10af556e5c72b61a53f81a8a15e7699148d15a by Thomas Friedrichsmeier.
Committed on 02/10/2022 at 11:32.
Pushed by tfry into branch 'master'.

Fix lockup condition while cancelling commands (expecting to pass all tests, on Windows, again)

M  +1    -1    .kde-ci.yml
M  +1    -0    ChangeLog
M  +28   -2    rkward/autotests/core_test.cpp
M  +6    -3    rkward/rbackend/rkrinterface.cpp

https://invent.kde.org/education/rkward/commit/2b10af556e5c72b61a53f81a8a15e7699148d15a

diff --git a/.kde-ci.yml b/.kde-ci.yml
index 0461eeb4..9c9d31aa 100644
--- a/.kde-ci.yml
+++ b/.kde-ci.yml
@@ -19,6 +19,6 @@ Dependencies:
     'frameworks/kcrash': '@stable'
 
 Options:
-  require-passing-tests-on: [ 'Linux', 'FreeBSD' ]
+  require-passing-tests-on: [ 'Linux', 'FreeBSD', 'Windows' ]
   tests-load-sensitve: True
   per-test-timeout: 120
diff --git a/ChangeLog b/ChangeLog
index c4794254..e1e29e54 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,4 @@
+- Fixed: In corner cases, cancelling commands could lead to a lockup
 - Fixed: IRT Cronbach's Alpha did not work for subsets, if the data.frame name contains dots
 - Fixed: Action to remove several rows in data editor, simultaneously, always remained disabled
 - Fixed: Workspace browser would not always show change, immediately, when object type changes
diff --git a/rkward/autotests/core_test.cpp b/rkward/autotests/core_test.cpp
index b2541f45..9d79b894 100644
--- a/rkward/autotests/core_test.cpp
+++ b/rkward/autotests/core_test.cpp
@@ -50,7 +50,7 @@ class RKWardCoreTest: public QObject {
 		bool *_done = &done;
 		connect(command->notifier(), &RCommandNotifier::commandFinished, this, [_done, callback](RCommand *command) { *_done = true; callback(command); });
 		RInterface::issueCommand(command, chain);
-		while (!done && t.elapsed() < timeoutms) {
+		while (!done && (t.elapsed() < timeoutms)) {
 			qApp->processEvents();
 		}
 		if (!done) {
@@ -98,7 +98,6 @@ private slots:
 		waitForAllFinished();
 	}
 	void initTestCase() {
-		qDebug("Initializing test case"); // Remove me. For diagnostics of test exception on Windows CI
 		qputenv("QTWEBENGINE_CHROMIUM_FLAGS", "--no-sandbox"); // Allow test to be run as root, which, for some reason is being done on the SuSE CI.
 		QLoggingCategory::setFilterRules("qt.text.layout=false");  // Filter out some noise
 		KAboutData::setApplicationData(KAboutData("rkward", "RKWard", RKWARD_VERSION, "Frontend to the R statistics language", KAboutLicense::GPL)); // component name needed for .rc files to load
@@ -213,6 +212,33 @@ private slots:
 		QCOMPARE(output.value(4), "444");
 	}
 
+	void cancelCommandStressTest() {
+		int cancelled_commands = 0;
+		int commands_out = 0;
+		for (int i = 0; i < 100; ++i) {
+			runCommandAsync(new RCommand("Sys.sleep(.005)", RCommand::User), nullptr, [&cancelled_commands, &commands_out](RCommand *command) {
+				if (command->wasCanceled()) cancelled_commands++;
+				commands_out++;
+			});
+			// We want to cover various cases, here, including cancelling commands before and after they have been sent to the backend, but also at least some commands that finish
+			// without being effictively cancelled.
+			if (i % 4 == 0) {
+				RInterface::instance()->cancelAll();
+			} else if (i % 4 == 1) {
+				while (commands_out < i) {
+					qApp->processEvents();
+				}
+			} else if (i % 4 == 2) {
+				qApp->processEvents();
+			}
+		}
+		waitForAllFinished();
+		// The point of this test case is to make sure, we do not get into a deadlock, however, the QVERIFYs below are to make sure the test itself behaves as expected.
+		// There needs to be some wiggle room, however, as this is inherently prone to race-conditions. (Commands finish running before getting cancelled, or they don't).
+		QVERIFY(cancelled_commands >= 25);
+		QVERIFY(cancelled_commands <= 75);
+	}
+
 	void priorityCommandTest() {
 		bool priority_command_done = false;
 		runCommandAsync(new RCommand("Sys.sleep(5)", RCommand::User), nullptr, [&priority_command_done](RCommand *command) {
diff --git a/rkward/rbackend/rkrinterface.cpp b/rkward/rbackend/rkrinterface.cpp
index 711026db..904d1312 100644
--- a/rkward/rbackend/rkrinterface.cpp
+++ b/rkward/rbackend/rkrinterface.cpp
@@ -199,9 +199,11 @@ void RInterface::tryNextCommand () {
 				command->status |= RCommand::Failed;
 
 				// notify ourselves...
-				RCommand* dummy = popPreviousCommand (command->id ());
-				RK_ASSERT (dummy == command);
-				handleCommandOut (command);
+				RCommand* dummy = popPreviousCommand(command->id ());
+				RK_ASSERT(dummy == command);
+				RK_DEBUG(RBACKEND, DL_DEBUG, "Not sending already cancelled command to backend");
+				handleCommandOut(command);
+				tryNextCommand();
 				return;
 			}
 
@@ -562,6 +564,7 @@ void RInterface::cancelCommand (RCommand *command) {
 	} else {
 		RK_ASSERT (false);
 	}
+	tryNextCommand();
 }
 
 void RInterface::pauseProcessing (bool pause) {


More information about the rkward-tracker mailing list