changeset 2573:3372c5255333 jobs

StoreScuJob, Orthanc Explorer for jobs
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 09 May 2018 17:56:14 +0200
parents 2e879c796ec7
children 84cbc5abf3cc
files Core/JobsEngine/IJob.h Core/JobsEngine/JobInfo.cpp Core/JobsEngine/JobInfo.h Core/JobsEngine/JobStatus.cpp Core/JobsEngine/JobStatus.h Core/JobsEngine/JobsEngine.cpp Core/JobsEngine/JobsEngine.h Core/JobsEngine/JobsRegistry.cpp Core/JobsEngine/JobsRegistry.h OrthancExplorer/explorer.html OrthancExplorer/explorer.js OrthancServer/OrthancRestApi/OrthancRestModalities.cpp OrthancServer/OrthancRestApi/OrthancRestSystem.cpp OrthancServer/ServerIndex.cpp UnitTestsSources/MultiThreadingTests.cpp
diffstat 15 files changed, 719 insertions(+), 380 deletions(-) [+]
line wrap: on
line diff
--- a/Core/JobsEngine/IJob.h	Mon May 07 21:42:04 2018 +0200
+++ b/Core/JobsEngine/IJob.h	Wed May 09 17:56:14 2018 +0200
@@ -56,6 +56,10 @@
 
     virtual float GetProgress() = 0;
 
-    virtual void GetDescription(Json::Value& value) = 0;
+    virtual void GetJobType(std::string& target) = 0;
+    
+    virtual void GetPublicContent(Json::Value& value) = 0;
+
+    virtual void GetInternalContent(Json::Value& value) = 0;
   };
 }
--- a/Core/JobsEngine/JobInfo.cpp	Mon May 07 21:42:04 2018 +0200
+++ b/Core/JobsEngine/JobInfo.cpp	Wed May 09 17:56:14 2018 +0200
@@ -62,7 +62,8 @@
       if (status_.GetProgress() > 0.01f &&
           ms > 0.01f)
       {
-        float remaining = boost::math::llround(1.0f - status_.GetProgress()) * ms;
+        float ratio = static_cast<float>(1.0 - status_.GetProgress());
+        long long remaining = boost::math::llround(ratio * ms);
         eta_ = timestamp_ + boost::posix_time::milliseconds(remaining);
         hasEta_ = true;
       }
@@ -115,7 +116,7 @@
   }
 
 
-  void JobInfo::Format(Json::Value& target) const
+  void JobInfo::Serialize(Json::Value& target) const
   {
     target = Json::objectValue;
     target["ID"] = id_;
@@ -125,9 +126,12 @@
     target["State"] = EnumerationToString(state_);
     target["Timestamp"] = boost::posix_time::to_iso_string(timestamp_);
     target["CreationTime"] = boost::posix_time::to_iso_string(creationTime_);
-    target["Runtime"] = static_cast<uint32_t>(runtime_.total_milliseconds());      
+    target["EffectiveRuntime"] = static_cast<double>(runtime_.total_milliseconds()) / 1000.0;
     target["Progress"] = boost::math::iround(status_.GetProgress() * 100.0f);
-    target["Description"] = status_.GetDescription();
+
+    target["Type"] = status_.GetJobType();
+    target["PublicContent"] = status_.GetPublicContent();
+    target["InternalContent"] = status_.GetInternalContent();
 
     if (HasEstimatedTimeOfArrival())
     {
--- a/Core/JobsEngine/JobInfo.h	Mon May 07 21:42:04 2018 +0200
+++ b/Core/JobsEngine/JobInfo.h	Wed May 09 17:56:14 2018 +0200
@@ -115,6 +115,6 @@
       return status_;
     }
 
-    void Format(Json::Value& target) const;
+    void Serialize(Json::Value& target) const;
   };
 }
--- a/Core/JobsEngine/JobStatus.cpp	Mon May 07 21:42:04 2018 +0200
+++ b/Core/JobsEngine/JobStatus.cpp	Wed May 09 17:56:14 2018 +0200
@@ -39,7 +39,9 @@
   JobStatus::JobStatus() :
     errorCode_(ErrorCode_InternalError),
     progress_(0),
-    description_(Json::objectValue)
+    jobType_("Invalid"),
+    publicContent_(Json::objectValue),
+    internalContent_(Json::objectValue)
   {
   }
 
@@ -47,7 +49,9 @@
   JobStatus::JobStatus(ErrorCode code,
                        IJob& job) :
     errorCode_(code),
-    progress_(job.GetProgress())
+    progress_(job.GetProgress()),
+    publicContent_(Json::objectValue),
+    internalContent_(Json::objectValue)
   {
     if (progress_ < 0)
     {
@@ -59,6 +63,8 @@
       progress_ = 1;
     }
 
-    job.GetDescription(description_);
+    job.GetJobType(jobType_);
+    job.GetPublicContent(publicContent_);
+    job.GetInternalContent(internalContent_);
   }
 }
--- a/Core/JobsEngine/JobStatus.h	Mon May 07 21:42:04 2018 +0200
+++ b/Core/JobsEngine/JobStatus.h	Wed May 09 17:56:14 2018 +0200
@@ -42,7 +42,9 @@
   private:
     ErrorCode      errorCode_;
     float          progress_;
-    Json::Value    description_;
+    std::string    jobType_;
+    Json::Value    publicContent_;
+    Json::Value    internalContent_;
 
   public:
     JobStatus();
@@ -60,9 +62,19 @@
       return progress_;
     }
 
-    const Json::Value& GetDescription() const
+    const std::string& GetJobType() const
+    {
+      return jobType_;
+    }
+
+    const Json::Value& GetPublicContent() const
     {
-      return description_;
+      return publicContent_;
+    }
+
+    const Json::Value& GetInternalContent() const
+    {
+      return internalContent_;
     }
   };
 }
--- a/Core/JobsEngine/JobsEngine.cpp	Mon May 07 21:42:04 2018 +0200
+++ b/Core/JobsEngine/JobsEngine.cpp	Wed May 09 17:56:14 2018 +0200
@@ -41,14 +41,18 @@
 
 namespace Orthanc
 {
+  bool JobsEngine::IsRunning()
+  {
+    boost::mutex::scoped_lock lock(stateMutex_);
+    return (state_ == State_Running);
+  }
+  
+  
   bool JobsEngine::ExecuteStep(JobsRegistry::RunningJob& running,
                                size_t workerIndex)
   {
     assert(running.IsValid());
 
-    LOG(INFO) << "Executing job with priority " << running.GetPriority()
-              << " in worker thread " << workerIndex << ": " << running.GetId();
-
     if (running.IsPauseScheduled())
     {
       running.GetJob().ReleaseResources();
@@ -94,10 +98,12 @@
     switch (result->GetCode())
     {
       case JobStepCode_Success:
+        running.GetJob().ReleaseResources();
         running.MarkSuccess();
         return false;
 
       case JobStepCode_Failure:
+        running.GetJob().ReleaseResources();
         running.MarkFailure();
         return false;
 
@@ -119,19 +125,9 @@
   {
     assert(engine != NULL);
 
-    for (;;)
+    while (engine->IsRunning())
     {
       boost::this_thread::sleep(boost::posix_time::milliseconds(200));
-
-      {
-        boost::mutex::scoped_lock lock(engine->stateMutex_);
-
-        if (engine->state_ != State_Running)
-        {
-          return;
-        }
-      }
-
       engine->GetRegistry().ScheduleRetries();
     }
   }
@@ -144,22 +140,16 @@
 
     LOG(INFO) << "Worker thread " << workerIndex << " has started";
 
-    for (;;)
+    while (engine->IsRunning())
     {
-      {
-        boost::mutex::scoped_lock lock(engine->stateMutex_);
-
-        if (engine->state_ != State_Running)
-        {
-          return;
-        }
-      }
-
       JobsRegistry::RunningJob running(engine->GetRegistry(), 100);
 
       if (running.IsValid())
       {
-        for (;;)
+        LOG(INFO) << "Executing job with priority " << running.GetPriority()
+                  << " in worker thread " << workerIndex << ": " << running.GetId();
+
+        while (engine->IsRunning())
         {
           if (!engine->ExecuteStep(running, workerIndex))
           {
--- a/Core/JobsEngine/JobsEngine.h	Mon May 07 21:42:04 2018 +0200
+++ b/Core/JobsEngine/JobsEngine.h	Wed May 09 17:56:14 2018 +0200
@@ -56,6 +56,8 @@
     boost::thread               retryHandler_;
     std::vector<boost::thread>  workers_;
 
+    bool IsRunning();
+    
     bool ExecuteStep(JobsRegistry::RunningJob& running,
                      size_t workerIndex);
     
--- a/Core/JobsEngine/JobsRegistry.cpp	Mon May 07 21:42:04 2018 +0200
+++ b/Core/JobsEngine/JobsRegistry.cpp	Wed May 09 17:56:14 2018 +0200
@@ -520,7 +520,7 @@
   }
 
 
-  void JobsRegistry::SetPriority(const std::string& id,
+  bool JobsRegistry::SetPriority(const std::string& id,
                                  int priority)
   {
     LOG(INFO) << "Changing priority to " << priority << " for job: " << id;
@@ -533,6 +533,7 @@
     if (found == jobsIndex_.end())
     {
       LOG(WARNING) << "Unknown job: " << id;
+      return false;
     }
     else
     {
@@ -553,13 +554,14 @@
           copy.pop();
         }
       }
+
+      CheckInvariants();
+      return true;
     }
-
-    CheckInvariants();
   }
 
 
-  void JobsRegistry::Pause(const std::string& id)
+  bool JobsRegistry::Pause(const std::string& id)
   {
     LOG(INFO) << "Pausing job: " << id;
 
@@ -571,6 +573,7 @@
     if (found == jobsIndex_.end())
     {
       LOG(WARNING) << "Unknown job: " << id;
+      return false;
     }
     else
     {
@@ -623,13 +626,14 @@
         default:
           throw OrthancException(ErrorCode_InternalError);
       }
+
+      CheckInvariants();
+      return true;
     }
-
-    CheckInvariants();
   }
 
 
-  void JobsRegistry::Resume(const std::string& id)
+  bool JobsRegistry::Resume(const std::string& id)
   {
     LOG(INFO) << "Resuming job: " << id;
 
@@ -641,23 +645,25 @@
     if (found == jobsIndex_.end())
     {
       LOG(WARNING) << "Unknown job: " << id;
+      return false;
     }
     else if (found->second->GetState() != JobState_Paused)
     {
       LOG(WARNING) << "Cannot resume a job that is not paused: " << id;
+      return false;
     }
     else
     {
       found->second->SetState(JobState_Pending);
       pendingJobs_.push(found->second);
       pendingJobAvailable_.notify_one();
+      CheckInvariants();
+      return true;      
     }
-
-    CheckInvariants();
   }
 
 
-  void JobsRegistry::Resubmit(const std::string& id)
+  bool JobsRegistry::Resubmit(const std::string& id)
   {
     LOG(INFO) << "Resubmitting failed job: " << id;
 
@@ -669,10 +675,12 @@
     if (found == jobsIndex_.end())
     {
       LOG(WARNING) << "Unknown job: " << id;
+      return false;
     }
     else if (found->second->GetState() != JobState_Failure)
     {
       LOG(WARNING) << "Cannot resubmit a job that has not failed: " << id;
+      return false;
     }
     else
     {
@@ -693,9 +701,10 @@
       found->second->SetState(JobState_Pending);
       pendingJobs_.push(found->second);
       pendingJobAvailable_.notify_one();
+
+      CheckInvariants();
+      return true;
     }
-
-    CheckInvariants();
   }
 
 
--- a/Core/JobsEngine/JobsRegistry.h	Mon May 07 21:42:04 2018 +0200
+++ b/Core/JobsEngine/JobsRegistry.h	Wed May 09 17:56:14 2018 +0200
@@ -130,14 +130,14 @@
     bool SubmitAndWait(IJob* job,        // Takes ownership
                        int priority);
     
-    void SetPriority(const std::string& id,
+    bool SetPriority(const std::string& id,
                      int priority);
 
-    void Pause(const std::string& id);
+    bool Pause(const std::string& id);
     
-    void Resume(const std::string& id);
+    bool Resume(const std::string& id);
 
-    void Resubmit(const std::string& id);
+    bool Resubmit(const std::string& id);
     
     void ScheduleRetries();
     
--- a/OrthancExplorer/explorer.html	Mon May 07 21:42:04 2018 +0200
+++ b/OrthancExplorer/explorer.html	Wed May 09 17:56:14 2018 +0200
@@ -37,7 +37,10 @@
     <div data-role="page" id="find-patients" >
       <div data-role="header" >
 	<h1><span class="orthanc-name"></span>Find a patient</h1>
-        <a href="#plugins" data-icon="grid" class="ui-btn-left" data-direction="reverse">Plugins</a>
+        <div data-type="horizontal" data-role="controlgroup" class="ui-btn-left"> 
+          <a href="#plugins" data-icon="grid" data-role="button" data-direction="reverse">Plugins</a>
+          <a href="#jobs" data-icon="refresh" data-role="button" data-direction="reverse">Jobs</a>
+        </div>
         <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> 
           <a href="#upload" data-icon="gear" data-role="button">Upload</a>
           <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a>
@@ -418,6 +421,43 @@
       </div>
     </div>
 
+    
+    <div data-role="page" id="jobs" >
+      <div data-role="header" >
+	<h1><span class="orthanc-name"></span>Jobs</h1>
+        <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
+      </div>
+      <div data-role="content">
+        <ul id="all-jobs" data-role="listview" data-inset="true" data-filter="true">
+        </ul>
+      </div>
+    </div>
+
+    <div data-role="page" id="job" >
+      <div data-role="header" >
+	<h1><span class="orthanc-name"></span>Job</h1>
+        <div data-type="horizontal" data-role="controlgroup" class="ui-btn-left"> 
+          <a href="#find-patients" data-icon="home" data-role="button" data-direction="reverse">Patients</a>
+          <a href="#jobs" data-icon="refresh" data-role="button" data-direction="reverse">Jobs</a>
+        </div>
+      </div>
+      <div data-role="content">
+        <ul data-role="listview" data-inset="true" data-filter="true" id="job-info">
+        </ul>
+
+        <fieldset class="ui-grid-b">
+          <div class="ui-block-a"></div>
+	  <div class="ui-block-b">
+            <button id="job-delete" data-theme="b">Delete job</button>         
+            <button id="job-retry" data-theme="b">Retry job</button>         
+            <button id="job-resubmit" data-theme="b">Resubmit job</button>         
+            <button id="job-pause" data-theme="b">Pause job</button>         
+            <button id="job-resume" data-theme="b">Resume job</button>         
+          </div>
+          <div class="ui-block-c"></div>
+	</fieldset>
+      </div>
+    </div>
 
     <div id="peer-store" style="display:none;" class="ui-body-c">
       <p align="center"><b>Sending to Orthanc peer...</b></p>
--- a/OrthancExplorer/explorer.js	Mon May 07 21:42:04 2018 +0200
+++ b/OrthancExplorer/explorer.js	Wed May 09 17:56:14 2018 +0200
@@ -1104,3 +1104,140 @@
     }
   });
 });
+
+
+
+function ParseJobTime(s)
+{
+  var t = (s.substr(0, 4) + '-' +
+           s.substr(4, 2) + '-' +
+           s.substr(6, 5) + ':' +
+           s.substr(11, 2) + ':' +
+           s.substr(13));
+  var utc = new Date(t);
+
+  // Convert from UTC to local time
+  return new Date(utc.getTime() - utc.getTimezoneOffset() * 60000);
+}
+
+
+function AddJobField(target, description, field)
+{
+  if (!(typeof field === 'undefined')) {
+    target.append($('<p>')
+                  .text(description)
+                  .append($('<strong>').text(field)));
+  }
+}
+
+
+function AddJobDateField(target, description, field)
+{
+  if (!(typeof field === 'undefined')) {
+    target.append($('<p>')
+                  .text(description)
+                  .append($('<strong>').text(ParseJobTime(field))));
+  }
+}
+
+
+$('#jobs').live('pagebeforeshow', function() {
+  $.ajax({
+    url: '../jobs?expand',
+    dataType: 'json',
+    async: false,
+    cache: false,
+    success: function(jobs) {
+      var target = $('#all-jobs');
+      $('li', target).remove();
+
+      jobs.map(function(job) {
+        var li = $('<li>');
+        var item = $('<a>');
+        li.append(item);
+        item.attr('href', '#job?uuid=' + job.ID);
+        item.append($('<h1>').text(job.Type));
+        item.append($('<span>').addClass('ui-li-count').text(job.State));
+        AddJobField(item, 'ID: ', job.ID);
+        AddJobField(item, 'Local AET: ', job.PublicContent.LocalAet);
+        AddJobField(item, 'Remote AET: ', job.PublicContent.RemoteAet);
+        AddJobDateField(item, 'Creation time: ', job.CreationTime);
+        AddJobDateField(item, 'Completion time: ', job.CompletionTime);
+        AddJobDateField(item, 'ETA: ', job.EstimatedTimeOfArrival);
+        target.append(li);
+      });
+
+      target.listview('refresh');
+    }
+  });
+});
+
+
+$('#job').live('pagebeforeshow', function() {
+  if ($.mobile.pageData) {
+    var pageData = DeepCopy($.mobile.pageData);
+
+    $.ajax({
+      url: '../jobs/' + pageData.uuid,
+      dataType: 'json',
+      async: false,
+      cache: false,
+      success: function(job) {
+        var target = $('#job-info');
+        $('li', target).remove();
+
+        target.append($('<li>')
+                      .attr('data-role', 'list-divider')
+                      .text('General information about the job'));
+
+        var block = $('<li>');
+        for (var i in job) {
+          if (i == 'CreationTime' ||
+              i == 'CompletionTime' ||
+              i == 'EstimatedTimeOfArrival') {
+            AddJobDateField(block, i + ': ', job[i]);
+          } else if (i != 'InternalContent' &&
+                     i != 'PublicContent' &&
+                     i != 'Timestamp') {
+            AddJobField(block, i + ': ', job[i]);
+          }
+        }
+
+        target.append(block);
+        
+        target.append($('<li>')
+                      .attr('data-role', 'list-divider')
+                      .text('Detailed information'));
+
+        var block = $('<li>');
+        for (var i in job.PublicContent) {
+          AddJobField(block, i + ': ', JSON.stringify(job.PublicContent[i]));
+        }
+
+        target.append(block);
+        
+        target.listview('refresh');
+
+        $('#job-delete').closest('.ui-btn').show();
+        $('#job-retry').closest('.ui-btn').hide();
+        $('#job-resubmit').closest('.ui-btn').hide();
+        $('#job-pause').closest('.ui-btn').hide();
+        $('#job-resume').closest('.ui-btn').hide();
+
+        if (job.State == 'Running' ||
+            job.State == 'Pending' ||
+            job.State == 'Retry') {
+          $('#job-pause').closest('.ui-btn').show();
+        }
+        else if (job.State == 'Success') {
+        }
+        else if (job.State == 'Failure') {
+          $('#job-resubmit').closest('.ui-btn').show();
+        }
+        else if (job.State == 'Paused') {
+          $('#job-resume').closest('.ui-btn').show();
+        }
+      }
+    });
+  }
+});
--- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Mon May 07 21:42:04 2018 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Wed May 09 17:56:14 2018 +0200
@@ -44,6 +44,352 @@
 #include "../QueryRetrieveHandler.h"
 #include "../ServerToolbox.h"
 
+
+
+namespace Orthanc
+{
+  class InstancesIteratorJob : public IJob
+  {
+  private:
+    bool                      started_;
+    std::vector<std::string>  instances_;
+    size_t                    position_;
+
+  public:
+    InstancesIteratorJob() :
+      started_(false),
+      position_(0)
+    {
+    }
+
+    void Reserve(size_t size)
+    {
+      if (started_)
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        instances_.reserve(size);
+      }
+    }
+
+    size_t GetInstancesCount() const
+    {
+      return instances_.size();
+    }
+    
+    void AddInstance(const std::string& instance)
+    {
+      if (started_)
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        instances_.push_back(instance);
+      }
+    }
+    
+    virtual void Start()
+    {
+      started_ = true;
+    }
+    
+    virtual float GetProgress()
+    {
+      if (instances_.size() == 0)
+      {
+        return 0;
+      }
+      else
+      {
+        return (static_cast<float>(position_) /
+                static_cast<float>(instances_.size()));
+      }
+    }
+
+    bool IsStarted() const
+    {
+      return started_;
+    }
+
+    bool IsDone() const
+    {
+      return (position_ >= instances_.size());
+    }
+
+    void Next()
+    {
+      if (IsDone())
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        position_ += 1;
+      }
+    }
+
+    const std::string& GetCurrentInstance() const
+    {
+      if (IsDone())
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        return instances_[position_];
+      }      
+    }
+  };
+
+
+  class StoreScuJob : public InstancesIteratorJob
+  {
+  private:
+    ServerContext&                      context_;
+    std::string                         localAet_;
+    RemoteModalityParameters            remote_;
+    bool                                permissive_;
+    std::string                         moveOriginatorAet_;
+    uint16_t                            moveOriginatorId_;
+    std::auto_ptr<DicomUserConnection>  connection_;
+    std::set<std::string>               failedInstances_;
+
+    void Open()
+    {
+      if (connection_.get() == NULL)
+      {
+        connection_.reset(new DicomUserConnection);
+        connection_->SetLocalApplicationEntityTitle(localAet_);
+        connection_->SetRemoteModality(remote_);
+        connection_->Open();
+      }
+    }
+    
+  public:
+    StoreScuJob(ServerContext& context) :
+      context_(context),
+      localAet_("ORTHANC"),
+      permissive_(false),
+      moveOriginatorId_(0)  // By default, not a C-MOVE
+    {
+    }
+
+    void AddResource(const std::string& publicId)
+    {
+      typedef std::list<std::string> Instances;
+
+      Instances instances;
+      context_.GetIndex().GetChildInstances(instances, publicId);
+
+      Reserve(GetInstancesCount() + instances.size());
+      
+      for (Instances::const_iterator it = instances.begin();
+           it != instances.end(); ++it)
+      {
+        AddInstance(*it);
+      }
+    }
+
+    const std::string& GetLocalAet() const
+    {
+      return localAet_;
+    }
+
+    void SetLocalAet(const std::string& aet)
+    {
+      if (IsStarted())
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        localAet_ = aet;
+      }
+    }
+
+    const RemoteModalityParameters& GetRemoteModality() const
+    {
+      return remote_;
+    }
+
+    void SetRemoteModality(const RemoteModalityParameters& remote)
+    {
+      if (IsStarted())
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        remote_ = remote;
+      }
+    }
+
+    bool IsPermissive() const
+    {
+      return permissive_;
+    }
+
+    void SetPermissive(bool permissive)
+    {
+      if (IsStarted())
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        permissive_ = permissive;
+      }
+    }
+
+    bool HasMoveOriginator() const
+    {
+      return moveOriginatorId_ != 0;
+    }
+    
+    const std::string& GetMoveOriginatorAet() const
+    {
+      if (HasMoveOriginator())
+      {
+        return moveOriginatorAet_;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+    }
+    
+    uint16_t GetMoveOriginatorId() const
+    {
+      if (HasMoveOriginator())
+      {
+        return moveOriginatorId_;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+    }
+
+    void SetMoveOriginator(const std::string& aet,
+                           int id)
+    {
+      if (IsStarted())
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else if (id < 0 || 
+               id >= 65536)
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+      else
+      {
+        moveOriginatorId_ = static_cast<uint16_t>(id);
+        moveOriginatorAet_ = aet;
+      }
+    }
+
+    virtual JobStepResult* ExecuteStep()
+    {
+      if (IsDone())
+      {
+        return new JobStepResult(JobStepCode_Success);
+      }
+
+      Open();
+
+      bool ok = false;
+      
+      try
+      {
+        std::string dicom;
+        context_.ReadDicom(dicom, GetCurrentInstance());
+
+        if (HasMoveOriginator())
+        {
+          connection_->Store(dicom, moveOriginatorAet_, moveOriginatorId_);
+        }
+        else
+        {
+          connection_->Store(dicom);
+        }
+
+        boost::this_thread::sleep(boost::posix_time::milliseconds(300));
+
+        ok = true;
+      }
+      catch (OrthancException& e)
+      {
+      }
+
+      if (!ok)
+      {
+        if (permissive_)
+        {
+          failedInstances_.insert(GetCurrentInstance());
+        }
+        else
+        {
+          return new JobStepResult(JobStepCode_Failure);
+        }
+      }
+
+      Next();
+
+      if (IsDone())
+      {
+        return new JobStepResult(JobStepCode_Success);
+      }
+      else
+      {
+        return new JobStepResult(JobStepCode_Continue);
+      }
+    }
+
+    virtual void ReleaseResources()   // For pausing jobs
+    {
+      connection_.reset(NULL);
+    }
+
+    virtual void GetJobType(std::string& target)
+    {
+      target = "C-Store";
+    }
+
+    virtual void GetPublicContent(Json::Value& value)
+    {
+      value["LocalAet"] = localAet_;
+      value["RemoteAet"] = remote_.GetApplicationEntityTitle();
+
+      if (HasMoveOriginator())
+      {
+        value["MoveOriginatorAET"] = GetMoveOriginatorAet();
+        value["MoveOriginatorID"] = GetMoveOriginatorId();
+      }
+
+      Json::Value v = Json::arrayValue;
+      for (std::set<std::string>::const_iterator it = failedInstances_.begin();
+           it != failedInstances_.end(); ++it)
+      {
+        v.append(*it);
+      }
+
+      value["FailedInstances"] = v;
+    }
+
+    virtual void GetInternalContent(Json::Value& value)
+    {
+      // TODO
+    }
+  };
+}
+
+
+
+
 namespace Orthanc
 {
   /***************************************************************************
@@ -689,6 +1035,7 @@
     bool asynchronous = Toolbox::GetJsonBooleanField(request, "Asynchronous", false);
     std::string moveOriginatorAET = Toolbox::GetJsonStringField(request, "MoveOriginatorAet", context.GetDefaultLocalApplicationEntityTitle());
     int moveOriginatorID = Toolbox::GetJsonIntegerField(request, "MoveOriginatorID", 0 /* By default, not a C-MOVE */);
+    int priority = Toolbox::GetJsonIntegerField(request, "Priority", 0);
 
     if (moveOriginatorID < 0 || 
         moveOriginatorID >= 65536)
@@ -698,6 +1045,44 @@
     
     RemoteModalityParameters p = Configuration::GetModalityUsingSymbolicName(remote);
 
+#if 1
+    std::auto_ptr<StoreScuJob> job(new StoreScuJob(context));
+    job->SetLocalAet(localAet);
+    job->SetRemoteModality(p);
+    job->SetPermissive(permissive);
+
+    if (moveOriginatorID != 0)
+    {
+      job->SetMoveOriginator(moveOriginatorAET, static_cast<uint16_t>(moveOriginatorID));
+    }
+
+    for (std::list<std::string>::const_iterator 
+           it = instances.begin(); it != instances.end(); ++it)
+    {
+      job->AddInstance(*it);
+    }
+
+    if (asynchronous)
+    {
+      // Asynchronous mode: Submit the job, but don't wait for its completion
+      std::string id;
+      context.GetJobsEngine().GetRegistry().Submit(id, job.release(), priority);
+
+      Json::Value v;
+      v["ID"] = id;
+      call.GetOutput().AnswerJson(v);
+    }
+    else if (context.GetJobsEngine().GetRegistry().SubmitAndWait(job.release(), priority))
+    {
+      // Synchronous mode: We have submitted and waited for completion
+      call.GetOutput().AnswerBuffer("{}", "application/json");
+    }
+    else
+    {
+      call.GetOutput().SignalError(HttpStatus_500_InternalServerError);
+    }
+    
+#else
     ServerJob job;
     for (std::list<std::string>::const_iterator 
            it = instances.begin(); it != instances.end(); ++it)
@@ -729,6 +1114,7 @@
     {
       call.GetOutput().SignalError(HttpStatus_500_InternalServerError);
     }
+#endif
   }
 
 
--- a/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp	Mon May 07 21:42:04 2018 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp	Wed May 09 17:56:14 2018 +0200
@@ -146,6 +146,24 @@
   }
 
 
+  static void GetDefaultEncoding(RestApiGetCall& call)
+  {
+    Encoding encoding = GetDefaultDicomEncoding();
+    call.GetOutput().AnswerBuffer(EnumerationToString(encoding), "text/plain");
+  }
+
+
+  static void SetDefaultEncoding(RestApiPutCall& call)
+  {
+    Encoding encoding = StringToEncoding(call.GetBodyData());
+
+    Configuration::SetDefaultEncoding(encoding);
+
+    call.GetOutput().AnswerBuffer(EnumerationToString(encoding), "text/plain");
+  }
+
+
+  
   // Plugins information ------------------------------------------------------
 
   static void ListPlugins(RestApiGetCall& call)
@@ -251,23 +269,65 @@
   }
 
 
-  static void GetDefaultEncoding(RestApiGetCall& call)
-  {
-    Encoding encoding = GetDefaultDicomEncoding();
-    call.GetOutput().AnswerBuffer(EnumerationToString(encoding), "text/plain");
-  }
 
 
-  static void SetDefaultEncoding(RestApiPutCall& call)
+  // Jobs information ------------------------------------------------------
+
+  static void ListJobs(RestApiGetCall& call)
   {
-    Encoding encoding = StringToEncoding(call.GetBodyData());
+    bool expand = call.HasArgument("expand");
+
+    Json::Value v = Json::arrayValue;
+
+    std::set<std::string> jobs;
+    OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().ListJobs(jobs);
 
-    Configuration::SetDefaultEncoding(encoding);
-
-    call.GetOutput().AnswerBuffer(EnumerationToString(encoding), "text/plain");
+    for (std::set<std::string>::const_iterator it = jobs.begin();
+         it != jobs.end(); ++it)
+    {
+      if (expand)
+      {
+        JobInfo info;
+        if (OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().GetJobInfo(info, *it))
+        {
+          Json::Value tmp;
+          info.Serialize(tmp);
+          v.append(tmp);
+        }
+      }
+      else
+      {
+        v.append(*it);
+      }
+    }
+    
+    call.GetOutput().AnswerJson(v);
   }
 
+  static void GetJobInfo(RestApiGetCall& call)
+  {
+    std::string id = call.GetUriComponent("id", "");
 
+    JobInfo info;
+    if (OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().GetJobInfo(info, id))
+    {
+      Json::Value json;
+      info.Serialize(json);
+      call.GetOutput().AnswerJson(json);
+    }
+  }
+
+  static void PauseJob(RestApiPostCall& call)
+  {
+    std::string id = call.GetUriComponent("id", "");
+
+    if (OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().Pause(id))
+    {
+      call.GetOutput().AnswerBuffer("{}", "application/json");
+    }
+  }
+
+  
   void OrthancRestApi::RegisterSystem()
   {
     Register("/", ServeRoot);
@@ -284,5 +344,9 @@
     Register("/plugins", ListPlugins);
     Register("/plugins/{id}", GetPlugin);
     Register("/plugins/explorer.js", GetOrthancExplorerPlugins);
+
+    Register("/jobs", ListJobs);
+    Register("/jobs/{id}", GetJobInfo);
+    Register("/jobs/{id}/pause", PauseJob);
   }
 }
--- a/OrthancServer/ServerIndex.cpp	Mon May 07 21:42:04 2018 +0200
+++ b/OrthancServer/ServerIndex.cpp	Wed May 09 17:56:14 2018 +0200
@@ -1212,7 +1212,7 @@
     ResourceType type;
     if (!db_.LookupResource(id, type, publicId))
     {
-      throw OrthancException(ErrorCode_InternalError);
+      throw OrthancException(ErrorCode_InexistentItem);
     }
 
     std::string patientId;
--- a/UnitTestsSources/MultiThreadingTests.cpp	Mon May 07 21:42:04 2018 +0200
+++ b/UnitTestsSources/MultiThreadingTests.cpp	Wed May 09 17:56:14 2018 +0200
@@ -603,321 +603,6 @@
 
 
 
-#include "../OrthancServer/ServerContext.h"
-
-namespace Orthanc
-{
-  class InstancesIteratorJob : public IJob
-  {
-  private:
-    bool                      started_;
-    std::vector<std::string>  instances_;
-    size_t                    position_;
-
-  public:
-    InstancesIteratorJob() :
-      started_(false),
-      position_(0)
-    {
-    }
-
-    void Reserve(size_t size)
-    {
-      if (started_)
-      {
-        throw OrthancException(ErrorCode_BadSequenceOfCalls);
-      }
-      else
-      {
-        instances_.reserve(size);
-      }
-    }
-    
-    void AddInstance(const std::string& instance)
-    {
-      if (started_)
-      {
-        throw OrthancException(ErrorCode_BadSequenceOfCalls);
-      }
-      else
-      {
-        instances_.push_back(instance);
-      }
-    }
-    
-    virtual void Start()
-    {
-      started_ = true;
-    }
-    
-    virtual float GetProgress()
-    {
-      if (instances_.size() == 0)
-      {
-        return 0;
-      }
-      else
-      {
-        return (static_cast<float>(position_) /
-                static_cast<float>(instances_.size()));
-      }
-    }
-
-    bool IsStarted() const
-    {
-      return started_;
-    }
-
-    bool IsDone() const
-    {
-      if (instances_.size() == 0)
-      {
-        return true;
-      }
-      else
-      {
-        return (position_ == instances_.size() - 1);
-      }
-    }
-
-    void Next()
-    {
-      if (IsDone())
-      {
-        throw OrthancException(ErrorCode_BadSequenceOfCalls);
-      }
-      else
-      {
-        position_ += 1;
-      }
-    }
-
-    const std::string& GetCurrentInstance() const
-    {
-      if (IsDone())
-      {
-        throw OrthancException(ErrorCode_BadSequenceOfCalls);
-      }
-      else
-      {
-        return instances_[position_];
-      }      
-    }
-  };
-
-
-  class StoreScuJob : public InstancesIteratorJob
-  {
-  private:
-    ServerContext&                      context_;
-    std::string                         localAet_;
-    RemoteModalityParameters            remote_;
-    bool                                permissive_;
-    std::string                         moveOriginatorAet_;
-    uint16_t                            moveOriginatorId_;
-    std::auto_ptr<DicomUserConnection>  connection_;
-    std::set<std::string>               failedInstances_;
-
-    void Open()
-    {
-      if (connection_.get() == NULL)
-      {
-        connection_.reset(new DicomUserConnection);
-        connection_->SetLocalApplicationEntityTitle(localAet_);
-        connection_->SetRemoteModality(remote_);
-        connection_->Open();
-      }
-    }
-    
-  public:
-    StoreScuJob(ServerContext& context) :
-      context_(context),
-      localAet_("ORTHANC"),
-      permissive_(false),
-      moveOriginatorId_(0)  // By default, not a C-MOVE
-    {
-    }
-
-    const std::string& GetLocalAet() const
-    {
-      return localAet_;
-    }
-
-    void SetLocalAet(const std::string& aet)
-    {
-      if (IsStarted())
-      {
-        throw OrthancException(ErrorCode_BadSequenceOfCalls);
-      }
-      else
-      {
-        localAet_ = aet;
-      }
-    }
-
-    const RemoteModalityParameters& GetRemoteModality() const
-    {
-      return remote_;
-    }
-
-    void SetRemoteModality(const RemoteModalityParameters& remote)
-    {
-      if (IsStarted())
-      {
-        throw OrthancException(ErrorCode_BadSequenceOfCalls);
-      }
-      else
-      {
-        remote_ = remote;
-      }
-    }
-
-    bool IsPermissive() const
-    {
-      return permissive_;
-    }
-
-    void SetPermissive(bool permissive)
-    {
-      if (IsStarted())
-      {
-        throw OrthancException(ErrorCode_BadSequenceOfCalls);
-      }
-      else
-      {
-        permissive_ = permissive;
-      }
-    }
-
-    bool HasMoveOriginator() const
-    {
-      return moveOriginatorId_ != 0;
-    }
-    
-    const std::string& GetMoveOriginatorAet() const
-    {
-      if (HasMoveOriginator())
-      {
-        return moveOriginatorAet_;
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_BadSequenceOfCalls);
-      }
-    }
-    
-    uint16_t GetMoveOriginatorId() const
-    {
-      if (HasMoveOriginator())
-      {
-        return moveOriginatorId_;
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_BadSequenceOfCalls);
-      }
-    }
-
-    void SetMoveOriginator(const std::string& aet,
-                           int id)
-    {
-      if (IsStarted())
-      {
-        throw OrthancException(ErrorCode_BadSequenceOfCalls);
-      }
-      else if (id < 0 || 
-               id >= 65536)
-      {
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-      else
-      {
-        moveOriginatorId_ = static_cast<uint16_t>(id);
-        moveOriginatorAet_ = aet;
-      }
-    }
-
-    virtual JobStepResult* ExecuteStep()
-    {
-      if (IsDone())
-      {
-        return new JobStepResult(JobStepCode_Success);
-      }
-
-      Open();
-
-      bool ok = false;
-      
-      try
-      {
-        std::string dicom;
-        context_.ReadDicom(dicom, GetCurrentInstance());
-
-        if (HasMoveOriginator())
-        {
-          connection_->Store(dicom, moveOriginatorAet_, moveOriginatorId_);
-        }
-        else
-        {
-          connection_->Store(dicom);
-        }
-
-        ok = true;
-      }
-      catch (OrthancException& e)
-      {
-      }
-
-      if (!ok)
-      {
-        if (permissive_)
-        {
-          failedInstances_.insert(GetCurrentInstance());
-        }
-        else
-        {
-          return new JobStepResult(JobStepCode_Failure);
-        }
-      }
-
-      Next();
-      
-      return new JobStepResult(IsDone() ? JobStepCode_Success : JobStepCode_Continue);
-    }
-
-    virtual void ReleaseResources()   // For pausing jobs
-    {
-      connection_.release();
-    }
-
-    virtual void GetDescription(Json::Value& value)
-    {
-      value["Type"] = "C-STORE";
-      value["LocalAet"] = localAet_;
-      
-      Json::Value v;
-      remote_.ToJson(v);
-      value["Target"] = v;
-
-      if (HasMoveOriginator())
-      {
-        value["MoveOriginatorAET"] = GetMoveOriginatorAet();
-        value["MoveOriginatorID"] = GetMoveOriginatorId();
-      }
-
-      v = Json::arrayValue;
-      for (std::set<std::string>::const_iterator it = failedInstances_.begin();
-           it != failedInstances_.end(); ++it)
-      {
-        v.append(*it);
-      }
-
-      value["FailedInstances"] = v;
-    }
-  };
-}
-
-
 
 TEST(JobsEngine, Basic)
 {