diff --git a/bulktest.php b/bulktest.php
index 0f8b7ff9..6b78720d 100644
--- a/bulktest.php
+++ b/bulktest.php
@@ -25,6 +25,8 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+namespace qtype_coderunner;
+
 define('NO_OUTPUT_BUFFERING', true);
 
 require_once(__DIR__ . '/../../../config.php');
@@ -37,15 +39,16 @@
 $repeatrandomonly = optional_param('repeatrandomonly', 1, PARAM_INT);
 $nruns = optional_param('nruns', 1, PARAM_INT);
 $questionids = optional_param('questionids', '', PARAM_RAW);  // A list of specific questions to check, eg, for rechecking failed tests.
+$clearcachefirst = optional_param('clearcachefirst', 0, PARAM_INT);
 
 
 // Login and check permissions.
 require_login();
-$context = context::instance_by_id($contextid);
+$context = \context::instance_by_id($contextid);
 require_capability('moodle/question:editall', $context);
 
 $urlparams = ['contextid' => $context->id, 'categoryid' => $categoryid, 'randomseed' => $randomseed,
-            'repeatrandomonly' => $repeatrandomonly, 'nruns' => $nruns, 'questionids' => $questionids];
+            'repeatrandomonly' => $repeatrandomonly, 'nruns' => $nruns, 'clearcachefirst' => $clearcachefirst, 'questionids' => $questionids];
 $PAGE->set_url('/question/type/coderunner/bulktest.php', $urlparams);
 $PAGE->set_context($context);
 $title = get_string('bulktesttitle', 'qtype_coderunner', $context->get_context_name());
@@ -66,12 +69,13 @@
 
 
 // Create the helper class.
-$bulktester = new qtype_coderunner_bulk_tester(
+$bulktester = new bulk_tester(
     $context,
     $categoryid,
     $randomseed,
     $repeatrandomonly,
-    $nruns
+    $nruns,
+    $clearcachefirst
 );
 
 // Was: Release the session, so the user can do other things while this runs.
@@ -83,6 +87,12 @@
 echo $OUTPUT->header();
 echo $OUTPUT->heading($title, 4);
 
+// Release the session, so the user can do other things while this runs.
+\core\session\manager::write_close();
+
+
+ini_set('memory_limit', '1024M');  // For big question banks - TODO: make this a setting?
+
 // Run the tests.
 if (count($questionids) == 0) {
     $bulktester->run_all_tests_for_context();
diff --git a/bulktestall.php b/bulktestall.php
index ee030f36..70c96e4b 100644
--- a/bulktestall.php
+++ b/bulktestall.php
@@ -23,6 +23,13 @@
  * @copyright 2016 Richard Lobb, The University of Canterbury
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
+namespace qtype_coderunner;
+
+use context;
+use context_system;
+use context_course;
+use html_writer;
+use moodle_url;
 
 define('NO_OUTPUT_BUFFERING', true);
 
@@ -59,7 +66,8 @@
 echo $OUTPUT->heading($title, 1);
 
 // Run the tests.
-$contextdata = qtype_coderunner_bulk_tester::get_num_coderunner_questions_by_context();
+ini_set('memory_limit', '2048M');  // For big question banks - TODO: make this a setting?
+$contextdata = bulk_tester::get_num_coderunner_questions_by_context();
 foreach ($contextdata as $contextid => $numcoderunnerquestions) {
     if ($skipping && $contextid != $startfromcontextid) {
         continue;
@@ -68,8 +76,9 @@
     $testcontext = context::instance_by_id($contextid);
     if (has_capability('moodle/question:editall', $context)) {
         $PAGE->set_context($testcontext);  // Helps grading cache pickup right course id.
-        $bulktester = new qtype_coderunner_bulk_tester($testcontext);
+        $bulktester = new bulk_tester($testcontext);
         echo $OUTPUT->heading(get_string('bulktesttitle', 'qtype_coderunner', $testcontext->get_context_name()));
+        echo html_writer::tag('p', 'Note: Grading cache not cleared -- do it from admin-plugins-cache if you really want to clear the cache for all course!');
         echo html_writer::tag('p', html_writer::link(
             new moodle_url(
                 '/question/type/coderunner/bulktestall.php',
@@ -86,5 +95,5 @@
 }
 
 // Display the final summary.
-qtype_coderunner_bulk_tester::print_summary_after_bulktestall($numpasses, $allfailingtests, $allmissinganswers);
+bulk_tester::print_summary_after_bulktestall($numpasses, $allfailingtests, $allmissinganswers);
 echo $OUTPUT->footer();
diff --git a/bulktestindex.php b/bulktestindex.php
index dc85c5ef..3e5bf2e6 100644
--- a/bulktestindex.php
+++ b/bulktestindex.php
@@ -13,6 +13,12 @@
 //
 // You should have received a copy of the GNU General Public License
 // along with Stack.  If not, see <http://www.gnu.org/licenses/>.
+namespace qtype_coderunner;
+
+use context_system;
+use context;
+use html_writer;
+use moodle_url;
 
 require_once(__DIR__ . '/../../../config.php');
 require_once($CFG->libdir . '/questionlib.php');
@@ -32,7 +38,7 @@
 }
 
 // Find in which contexts the user can edit questions.
-$questionsbycontext = qtype_coderunner_bulk_tester::get_num_coderunner_questions_by_context();
+$questionsbycontext = bulk_tester::get_num_coderunner_questions_by_context();
 $availablequestionsbycontext = [];
 foreach ($questionsbycontext as $contextid => $numcoderunnerquestions) {
     $context = context::instance_by_id($contextid);
@@ -56,7 +62,7 @@
 echo <<<HTML
 <div class="bulk-test-config" style="margin-bottom: 20px; padding: 10px; background-color: #f5f5f5; border: 1px solid #ddd;">
     <h3>Test Configuration</h3>
-    <div style="margin-bottom: 10px; display: grid; grid-template-columns: auto 80px; gap: 10px; align-items: center; max-width: 240px;">
+    <div style="margin-bottom: 10px; display: grid; grid-template-columns: auto 80px; gap: 10px; align-items: center; max-width:400px;">
         <label for="nruns">Number of runs:</label>
         <input type="number" id="nruns" value="{$nruns}" min="1" style="width: 80px;">
 
@@ -67,6 +73,10 @@
         <div>
             <input type="checkbox" id="repeatrandomonly" checked>
         </div>
+        <label for="clearcachefirst">Clear course grading cache first (be careful):</label>
+        <div>
+            <input type="checkbox" id="clearcachefirst" onchange="confirmCheckboxChange(this)">
+        </div>
     </div>
 </div>
 HTML;
@@ -77,7 +87,8 @@
 } else {
     echo get_string('bulktestinfo', 'qtype_coderunner');
     echo $OUTPUT->heading(get_string('coderunnercontexts', 'qtype_coderunner'));
-
+    $jobehost = get_config('qtype_coderunner', 'jobe_host');
+    echo html_writer::tag('p', '<b>jobe_host:</b> ' . $jobehost);
     echo html_writer::start_tag('ul');
     $buttonstyle = 'background-color: #FFFFD0; padding: 2px 2px 0px 2px;border: 4px solid white';
     foreach ($availablequestionsbycontext as $name => $info) {
@@ -87,7 +98,8 @@
         $testallstr = get_string('bulktestallincontext', 'qtype_coderunner');
         $testalltitledetails = ['title' => get_string('testalltitle', 'qtype_coderunner'), 'style' => $buttonstyle];
         $testallspan = html_writer::tag(
-            'span', $testallstr,
+            'span',
+            $testallstr,
             ['class' => 'test-link',
              'data-contextid' => $contextid,
              'style' => $buttonstyle . ';cursor:pointer;']
@@ -106,14 +118,16 @@
         echo html_writer::start_tag('li', ['class' => $class]);
         echo $litext;
 
-        $categories = qtype_coderunner_bulk_tester::get_categories_for_context($contextid);
+        $categories = bulk_tester::get_categories_for_context($contextid);
         echo html_writer::start_tag('ul', ['class' => 'expandable']);
 
         $titledetails = ['title' => get_string('testallincategory', 'qtype_coderunner')];
         foreach ($categories as $cat) {
             if ($cat->count > 0) {
                 $linktext = $cat->name . ' (' . $cat->count . ')';
-                $span = html_writer::tag('span', $linktext,
+                $span = html_writer::tag(
+                    'span',
+                    $linktext,
                     ['class' => 'test-link',
                      'data-contextid' => $contextid,
                      'data-categoryid' => $cat->id,
@@ -127,7 +141,9 @@
     }
 
     echo html_writer::end_tag('ul');
-
+    echo html_writer::empty_tag('br');
+    echo html_writer::tag('hr', '');
+    echo html_writer::empty_tag('br');
     if (has_capability('moodle/site:config', context_system::instance())) {
         echo html_writer::tag('p', html_writer::link(
             new moodle_url('/question/type/coderunner/bulktestall.php'),
@@ -138,6 +154,17 @@
 
 echo <<<SCRIPT_END
 <script>
+function confirmCheckboxChange(checkbox) {
+    if (checkbox.checked) {
+        var prompt = "Are you sure you want to clear the cache for the selected course?";
+        prompt = prompt + " This will clear the cache for all attempts on all questions!";
+        const confirmed = confirm(prompt);
+        if (!confirmed) {
+            checkbox.checked = false;
+        }
+    }
+}
+
 document.addEventListener("DOMContentLoaded", function(event) {
     // Handle expandable sections
     var expandables = document.getElementsByClassName('expandable');
@@ -169,18 +196,19 @@
             var nruns = document.getElementById('nruns').value;
             var randomseed = document.getElementById('randomseed').value;
             var repeatrandomonly = document.getElementById('repeatrandomonly').checked ? 1 : 0;
+            var clearcachefirst = document.getElementById('clearcachefirst').checked ? 1 : 0;
 
             // Build URL parameters
             var params = new URLSearchParams();
             params.append('contextid', link.dataset.contextid);
-            params.append('randomseed', randomseed);
-            params.append('repeatrandomonly', repeatrandomonly);
-            params.append('nruns', nruns);
-
             // Add category ID if present
             if (link.dataset.categoryid) {
                 params.append('categoryid', link.dataset.categoryid);
             }
+            params.append('nruns', nruns);
+            params.append('randomseed', randomseed);
+            params.append('repeatrandomonly', repeatrandomonly);
+            params.append('clearcachefirst', clearcachefirst);
 
             // Construct and navigate to URL
             var url = M.cfg.wwwroot + '/question/type/coderunner/bulktest.php?' + params.toString();
diff --git a/classes/bulk_tester.php b/classes/bulk_tester.php
index c28692e5..ff9cc6dc 100644
--- a/classes/bulk_tester.php
+++ b/classes/bulk_tester.php
@@ -26,7 +26,15 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-class qtype_coderunner_bulk_tester {
+namespace qtype_coderunner;
+
+use moodle_url;
+use html_writer;
+use question_bank;
+use core_php_time_limit;
+use question_state;
+
+class bulk_tester {
     /** @var context Context to run bulktester for. */
     public $context;
 
@@ -42,6 +50,9 @@ class qtype_coderunner_bulk_tester {
     /** @var int How many runs to do for each question. */
     public $nruns;
 
+    /** @var int Whether or not to clear the grading cache for this context first Default: 0 . */
+    public $clearcachefirst;
+
     /** @var int The number of questions that passed tests. */
     public $numpasses;
 
@@ -78,23 +89,26 @@ class qtype_coderunner_bulk_tester {
      * @param int $repeatrandomonly when true(or 1), only repeats tests for questions with random in the name.
      *              Default = true (or really 1).
      * @param int $nruns the number times to test each question. Default to 1.
+     * @param int $clearcachefirst If 1 then clears the grading cache (ignoring ttl) for the given context before running the tests. Default is 0.
      */
     public function __construct(
         $context = null,
         $categoryid = null,
         $randomseed = -1,
         $repeatrandomonly = 1,
-        $nruns = 1
+        $nruns = 1,
+        $clearcachefirst = 0
     ) {
         if ($context === null) {
             $site = get_site(); // Get front page course.
-             $context = context_course::instance($site->id);
+             $context = \context_course::instance($site->id);
         }
         $this->context = $context;
         $this->categoryid = $categoryid;
         $this->randomseed = $randomseed;
         $this->repeatrandomonly = $repeatrandomonly;
         $this->nruns = $nruns;
+        $this->clearcachefirst = $clearcachefirst;
         $this->numpasses = 0;
         $this->numfails = 0;
         $this->failedquestionids = [];
@@ -316,6 +330,13 @@ public function run_all_tests_for_context($questionidstoinclude = []) {
          $questiontestsurl = new moodle_url('/question/type/coderunner/questiontestrun.php');
         $questiontestsurl->params($qparams);
 
+        // Clear grading cache if requested. usettl is set to false here.
+        if ($this->clearcachefirst) {
+            $purger = new cache_purger($this->context->id, false);
+            $purger->purge_cache_for_context();
+        }
+        $jobehost = get_config('qtype_coderunner', 'jobe_host');
+        echo html_writer::tag('p', '<b>jobe_host:</b> ' . $jobehost);
         $this->numpasses = 0;
         foreach ($categories as $currentcategoryid => $nameandcount) {
             $categoryname = $nameandcount->name;
@@ -356,7 +377,7 @@ public function run_all_tests_for_context($questionidstoinclude = []) {
                 for ($i = 0; $i < $nrunsthistime; $i++) {
                     // Only records last outcome and message.
                     try {
-                        [$outcome, $message] = $this->load_and_test_question($question->id);
+                         [$outcome, $message] = $this->load_and_test_question($question->id);
                     } catch (Exception $e) {
                         $message = $e->getMessage();
                         $outcome = self::FAIL;
@@ -385,7 +406,7 @@ public function run_all_tests_for_context($questionidstoinclude = []) {
                 }
                 echo "</li>";
                 gc_collect_cycles(); // Because PHP's default memory management is rubbish.
-                flush(); // Force output tmemory_limito prevent timeouts and show progress.
+                flush(); // Force output to prevent timeouts and show progress.
                 $qparams['category'] = $currentcategoryid . ',' . $this->context->id;
                 $qparams['lastchanged'] = $question->id;
                 $qparams['qperpage'] = 1000;
@@ -431,7 +452,7 @@ private function load_and_test_question($questionid) {
                     $status = self::FAIL;
                 }
             }
-        } catch (qtype_coderunner_exception $e) {
+        } catch (exception $e) {
             if (isset($question)) {
                 $questionname = ' ' . format_string($question->name);
             } else {
@@ -462,7 +483,7 @@ private function test_question($question) {
         if (!empty($params['answer_language'])) {
             $response['language'] = $params['answer_language'];
         } else if (!empty($question->acelang) && strpos($question->acelang, ',') !== false) {
-            [$languages, $defaultlang] = qtype_coderunner_util::extract_languages($question->acelang);
+            [$languages, $defaultlang] = util::extract_languages($question->acelang);
             if ($defaultlang === '') {
                 $defaultlang = $languages[0];
             }
@@ -471,7 +492,7 @@ private function test_question($question) {
         try {
             [$fraction, $state] = $question->grade_response($response, false);
             $ok = $state == question_state::$gradedright;
-        } catch (qtype_coderunner_exception $e) {
+        } catch (exception $e) {
             $ok = false; // If user clicks link to see why, they'll get the same exception.
         }
         return $ok;
diff --git a/classes/jobesandbox.php b/classes/jobesandbox.php
index c4ba6a03..f00a3775 100644
--- a/classes/jobesandbox.php
+++ b/classes/jobesandbox.php
@@ -162,7 +162,7 @@ public function execute($sourcecode, $language, $input, $files = null, $params =
         try {
             // Had to use try here as isset($PAGE->context) always seems to fail even if the context has been set.
             $context = $PAGE->context;
-            $courseid = $context->get_course_context(true)->instanceid;  // raises exception if context is unknown.
+            $courseid = $context->get_course_context(true)->instanceid;  // Raises exception if context is unknown.
         } catch (Exception $e) {
             $courseid = 1; // Use context of 1 as no $PAGE context is set, eg, could be a websocket UI run.
         }
@@ -229,7 +229,8 @@ public function execute($sourcecode, $language, $input, $files = null, $params =
             }
         }
 
-
+        // Add jobserver name(s) to runspec so jobs with different jobeservers are treated as different.
+        $runspec['jobeserver'] = $this->jobeserver;
         $cache = cache::make('qtype_coderunner', 'coderunner_grading_cache');
         $runresult = null;
         if (get_config('qtype_coderunner', 'enablegradecache') && $usecache) {
diff --git a/db/caches.php b/db/caches.php
index ce6fbfc8..27112490 100644
--- a/db/caches.php
+++ b/db/caches.php
@@ -30,7 +30,7 @@
 $definitions = [
     'coderunner_grading_cache' => [
         'mode' => cache_store::MODE_APPLICATION,
-        'maxsize' => 50000000, // This will be ignored by the standard file cache
+        'maxsize' => 50000000, // This will be ignored by the standard file cache.
         'simplekeys' => true,
         'simpledata' => false,
         'canuselocalstore' => true,
diff --git a/downloadquizattempts.php b/downloadquizattempts.php
index f5b2e9b1..90d529af 100644
--- a/downloadquizattempts.php
+++ b/downloadquizattempts.php
@@ -27,15 +27,21 @@
  * @copyright 2017 Richard Lobb, The University of Canterbury
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
+namespace qtype_coderunner;
+
+use context_system;
+use context_course;
+use html_writer;
+use moodle_url;
+
+require_once(__DIR__ . '/../../../config.php');
+require_once($CFG->libdir . '/questionlib.php');
 
 define('NO_OUTPUT_BUFFERING', true);
 if (!defined('ANONYMISE')) {
     define('ANONYMISE', 0);
 }
 
-require_once(__DIR__ . '/../../../config.php');
-require_once($CFG->libdir . '/questionlib.php');
-
 // Login and check permissions.
 $context = context_system::instance();
 require_login();
@@ -47,7 +53,7 @@
 $PAGE->requires->jquery_plugin('ui');
 $PAGE->requires->jquery_plugin('ui-css');
 
-$courses = qtype_coderunner_bulk_tester::get_all_courses();
+$courses = bulk_tester::get_all_courses();
 
 // Start display.
 echo $OUTPUT->header();
diff --git a/findduplicates.php b/findduplicates.php
index c5ab2807..11e4394e 100644
--- a/findduplicates.php
+++ b/findduplicates.php
@@ -25,6 +25,9 @@
  * @copyright 2018 and beyond Richard Lobb, The University of Canterbury
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
+namespace qtype_coderunner;
+
+use context;
 
 define('NO_OUTPUT_BUFFERING', true);
 
@@ -57,7 +60,7 @@
 echo "<table class='table table-bordered table-striped'>\n";
 echo "<tr><th>Q1 name</th><th>Q1 Category</th><th>Q2 name</th><th>Q2 category</th></tr>\n";
 // Find all the duplicates.
-$allquestionsmap = qtype_coderunner_bulk_tester::get_all_coderunner_questions_in_context($contextid);
+$allquestionsmap = bulk_tester::get_all_coderunner_questions_in_context($contextid);
 $allquestions = array_values($allquestionsmap);
 $numduplicates = 0;
 for ($i = 0; $i < count($allquestions); $i++) {
diff --git a/findduplicatesindex.php b/findduplicatesindex.php
index c134a12c..de8c6298 100644
--- a/findduplicatesindex.php
+++ b/findduplicatesindex.php
@@ -24,6 +24,13 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+namespace qtype_coderunner;
+
+use context;
+use context_system;
+use html_writer;
+use moodle_url;
+
 require_once(__DIR__ . '/../../../config.php');
 require_once($CFG->libdir . '/questionlib.php');
 
@@ -40,7 +47,7 @@
 echo $OUTPUT->heading('Courses containing CodeRunner questions');
 
 // Find in which contexts the user can edit questions.
-$questionsbycontext = qtype_coderunner_bulk_tester::get_num_coderunner_questions_by_context();
+$questionsbycontext = bulk_tester::get_num_coderunner_questions_by_context();
 $availablequestionsbycontext = [];
 foreach ($questionsbycontext as $contextid => $numcoderunnerquestions) {
     $context = context::instance_by_id($contextid);
diff --git a/prototypeusage.php b/prototypeusage.php
index 5922f4b1..d0b32ddc 100644
--- a/prototypeusage.php
+++ b/prototypeusage.php
@@ -24,6 +24,14 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+namespace qtype_coderunner;
+
+use context;
+use context_system;
+use html_writer;
+use moodle_url;
+use qtype_coderunner;
+
 define('NO_OUTPUT_BUFFERING', true);
 
 require_once(__DIR__ . '/../../../config.php');
@@ -44,7 +52,7 @@
 $PAGE->set_title(get_string('prototypeusage', 'qtype_coderunner'));
 
 // Create the helper class.
-$bulktester = new qtype_coderunner_bulk_tester();
+$bulktester = new bulk_tester();
 
 // Start display.
 echo $OUTPUT->header();
diff --git a/prototypeusageindex.php b/prototypeusageindex.php
index 99192b3c..52378e1d 100644
--- a/prototypeusageindex.php
+++ b/prototypeusageindex.php
@@ -26,6 +26,14 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+namespace qtype_coderunner;
+
+use context;
+use context_system;
+use context_course;
+use html_writer;
+use moodle_url;
+
 require_once(__DIR__ . '/../../../config.php');
 require_once($CFG->libdir . '/questionlib.php');
 
@@ -37,7 +45,7 @@
 $PAGE->set_context($context);
 $PAGE->set_title(get_string('prototypeusageindex', 'qtype_coderunner'));
 
-$allcourses = qtype_coderunner_bulk_tester::get_all_courses();
+$allcourses = bulk_tester::get_all_courses();
 
 // Start display.
 echo $OUTPUT->header();