Repository: Qiskit/qiskit

Issue:
Trying to cancel a job just after initialization raises
### Steps to reproduce the problem
1. Create an `IBMQJob` by running a `qobj` in a backend.
2. Immediately after, call the `cancel()` method of the `IBMQJob` instance.

### What is the expected behaviour?
Not throwing or, at least, not raising a gave exception but prevent the cancellation.

### What is the current behaviour?
The method `cancel()` raises.

### Suggested solutions
Detect if the job is still initializing and reject the cancellation.




Discussion:
I'm not sure I understand when you are suggesting to reject `cancel()`? It makes sense to me that we allow cancellation up until it can't be cancelled on the server anymore. This time point is different for hubs and non-hubs. 

If you are referring to not raising if an error message comes back from the server, I think I may agree. Maybe instead should do, 
```
self._status = JobStatus.ERROR
self._status_msg = str(self._exception)
```

Resolving patch:
diff --git a/qiskit/backends/ibmq/ibmqjob.py b/qiskit/backends/ibmq/ibmqjob.py
--- a/qiskit/backends/ibmq/ibmqjob.py
+++ b/qiskit/backends/ibmq/ibmqjob.py
@@ -19,6 +19,8 @@
 import datetime
 import numpy
 
+from IBMQuantumExperience import ApiError
+
 from qiskit.qobj import qobj_to_dict
 from qiskit.transpiler import transpile
 from qiskit.backends import BaseJob
@@ -151,30 +153,38 @@ def result(self, timeout=None, wait=5):
         return this_result
 
     def cancel(self):
-        """Attempt to cancel job. Currently this is only possible on
-        commercial systems.
+        """Attempt to cancel a job. If it is not possible, check the
+        ```exception``` property for more information.
+
         Returns:
-            bool: True if job can be cancelled, else False.
+            bool: True if job can be cancelled, else False. Currently this is
+            only possible on commercial systems.
 
         Raises:
             IBMQJobError: if server returned error
         """
-        if self._is_commercial:
-            hub = self._api.config['hub']
-            group = self._api.config['group']
-            project = self._api.config['project']
+        hub = self._api.config.get('hub', None)
+        group = self._api.config.get('group', None)
+        project = self._api.config.get('project', None)
+
+        cancelled = False
+        try:
             response = self._api.cancel_job(self._id, hub, group, project)
-            if 'error' in response:
+            errored = 'error' in response
+            cancelled = not errored
+
+            if errored:
                 err_msg = response.get('error', '')
                 error = IBMQJobError('Error cancelling job: %s' % err_msg)
                 self._exception = error
-                raise error
-            else:
-                self._cancelled = True
-                return True
-        else:
-            self._cancelled = False
-            return False
+
+        except ApiError as error:
+            err_msg = error.usr_msg
+            error = IBMQJobError('Error cancelling job: %s' % err_msg)
+            self._exception = error
+
+        self._cancelled = cancelled
+        return self._cancelled
 
     @property
     def status(self):
@@ -330,12 +340,6 @@ def exception(self):
             self._status_msg = str(self._exception)
         return self._exception
 
-    @property
-    def _is_commercial(self):
-        config = self._api.config
-        # this check may give false positives so should probably be improved
-        return config.get('hub') and config.get('group') and config.get('project')
-
     @property
     def id(self):
         """


Test patch:
diff --git a/test/python/test_ibmqjob_states.py b/test/python/test_ibmqjob_states.py
--- a/test/python/test_ibmqjob_states.py
+++ b/test/python/test_ibmqjob_states.py
@@ -121,15 +121,15 @@ def test_status_flow_for_non_cancellable_job(self):
         self._current_api.progress()
         self.assertStatus(job, JobStatus.RUNNING)
 
-    def test_status_flow_for_throwing_cancellation(self):
-        job = self.run_with_api(ThrowingCancellationAPI())
+    def test_status_flow_for_errored_cancellation(self):
+        job = self.run_with_api(ErroredCancellationAPI())
         self.assertStatus(job, JobStatus.INITIALIZING)
 
         self.wait_for_initialization(job)
         self.assertStatus(job, JobStatus.RUNNING)
 
-        with self.assertRaises(IBMQJobError):
-            job.cancel()
+        can_cancel = job.cancel()
+        self.assertFalse(can_cancel)
         self.assertIsInstance(job.exception, IBMQJobError)
 
         self.assertStatus(job, JobStatus.RUNNING)
@@ -224,11 +224,11 @@ def test_never_complete_result_with_timeout(self):
         self.assertEqual(job.result(timeout=1).get_status(), 'ERROR')
         self.assertStatus(job, JobStatus.RUNNING)
 
-    @unittest.expectedFailure
-    def test_cancel_while_initializing_does_not_fail(self):
+    def test_cancel_while_initializing_is_not_possible_but_does_not_fail(self):
         job = self.run_with_api(CancellableAPI())
-        job.cancel()
-        self.assertStatus(job, JobStatus.CANCELLED)
+        can_cancel = job.cancel()
+        self.assertFalse(can_cancel)
+        self.assertStatus(job, JobStatus.INITIALIZING)
 
     def test_only_final_states_cause_datailed_request(self):
         from unittest import mock
@@ -465,7 +465,7 @@ class NonCancellableAPI(BaseFakeAPI):
     ]
 
 
-class ThrowingCancellationAPI(BaseFakeAPI):
+class ErroredCancellationAPI(BaseFakeAPI):
     """Class for emulating an API with cancellation but throwing while
     trying."""