From 62ddb2cf1c98be08cc6a8fa48f2753105cae7691 Mon Sep 17 00:00:00 2001 From: kevin-tian Date: Wed, 29 Oct 2025 13:40:27 -0400 Subject: [PATCH 1/6] Raise warning if instance limit is reached --- qiskit_ibm_runtime/qiskit_runtime_service.py | 22 ++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/qiskit_ibm_runtime/qiskit_runtime_service.py b/qiskit_ibm_runtime/qiskit_runtime_service.py index 6bd6d23b4..e7632e0c2 100644 --- a/qiskit_ibm_runtime/qiskit_runtime_service.py +++ b/qiskit_ibm_runtime/qiskit_runtime_service.py @@ -967,6 +967,7 @@ def _run( IBMRuntimeError: An error occurred running the program. """ + self._check_instance_usage() qrt_options: RuntimeOptions = options if options is None: qrt_options = RuntimeOptions() @@ -1192,6 +1193,27 @@ def usage(self) -> Dict[str, Any]: usage_dict["usage_remaining_seconds"] = usage_remaining return usage_dict + def _check_instance_usage(self) -> None: + """Raise warning if instance usage has been reached.""" + usage_dict = self._active_api_client.cloud_usage() + limit_reached = usage_dict.get("usage_limit_reached", False) + usage_remaining = usage_dict.get( + "usage_limit_seconds", usage_dict.get("usage_allocation_seconds") + ) - usage_dict.get("usage_consumed_seconds", 0) + + if limit_reached: + if not usage_dict.get("usage_limit_seconds") or usage_remaining > 0: + warnings.warn( + "There is currently no more time available for this instance’s plan on the account. " + "Workloads will not run until time is made available. " + "Check https://quantum.cloud.ibm.com/instances for more details." + ) + if usage_dict.get("usage_limit_seconds") and usage_remaining <= 0: + warnings.warn( + "This instance has met its usage limit. Workloads will not run until time is made " + "available. Check https://quantum.cloud.ibm.com/instances for more details." + ) + def _decode_job(self, raw_data: Dict) -> RuntimeJobV2: """Decode job data received from the server. From 6ddae7f50bd6fd98a32982dc78da628937913d10 Mon Sep 17 00:00:00 2001 From: kevin-tian Date: Wed, 29 Oct 2025 14:12:06 -0400 Subject: [PATCH 2/6] Add reno & fix tests --- release-notes/unreleased/2458.feat.rst | 2 ++ test/unit/mock/fake_runtime_client.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 release-notes/unreleased/2458.feat.rst diff --git a/release-notes/unreleased/2458.feat.rst b/release-notes/unreleased/2458.feat.rst new file mode 100644 index 000000000..22e3eb3d8 --- /dev/null +++ b/release-notes/unreleased/2458.feat.rst @@ -0,0 +1,2 @@ +When a job is submitted, there will now be warnings if the instance being used +has reached its limit. \ No newline at end of file diff --git a/test/unit/mock/fake_runtime_client.py b/test/unit/mock/fake_runtime_client.py index 31227b24d..43e049a2c 100644 --- a/test/unit/mock/fake_runtime_client.py +++ b/test/unit/mock/fake_runtime_client.py @@ -451,6 +451,20 @@ def session_details(self, session_id: str) -> Dict[str, Any]: """Return the details of the session.""" return {"id": session_id, "mode": "dedicated", "backend_name": "common_backend"} + def cloud_usage(self) -> Dict[str, Any]: + """Return cloud instance usage information.""" + return { + "instance_id": "instance_id", + "plan_id": "plan_id", + "usage_consumed_seconds": 6000, + "usage_period": { + "start_time": "2025-10-01T17:40:06.269Z", + "end_time": "2025-10-29T17:40:06.269Z", + }, + "usage_allocation_seconds": 90000, + "usage_remaining_seconds": 84000, + } + def _find_backend(self, backend_name): for back in self._backends: if back.name == backend_name: From b7f562036b41b8e2a252129dfc9b13d149dcdc22 Mon Sep 17 00:00:00 2001 From: kevin-tian Date: Wed, 29 Oct 2025 15:05:16 -0400 Subject: [PATCH 3/6] Add url to warning msg & add test --- qiskit_ibm_runtime/qiskit_runtime_service.py | 10 ++++--- test/unit/mock/fake_runtime_client.py | 1 + test/unit/test_jobs.py | 28 ++++++++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/qiskit_ibm_runtime/qiskit_runtime_service.py b/qiskit_ibm_runtime/qiskit_runtime_service.py index e7632e0c2..5e88c5e80 100644 --- a/qiskit_ibm_runtime/qiskit_runtime_service.py +++ b/qiskit_ibm_runtime/qiskit_runtime_service.py @@ -16,6 +16,7 @@ import warnings from datetime import datetime from typing import Dict, Callable, Optional, Union, List, Any, Type, Sequence, Tuple +from urllib.parse import quote from qiskit.providers.backend import BackendV2 as Backend from qiskit.providers.exceptions import QiskitBackendNotFoundError @@ -1205,13 +1206,16 @@ def _check_instance_usage(self) -> None: if not usage_dict.get("usage_limit_seconds") or usage_remaining > 0: warnings.warn( "There is currently no more time available for this instance’s plan on the account. " - "Workloads will not run until time is made available. " - "Check https://quantum.cloud.ibm.com/instances for more details." + "Workloads will not run until time is made available. Check " + f"https://quantum.cloud.ibm.com/instances/{quote(self.active_instance(), safe="")} " + "for more details." ) if usage_dict.get("usage_limit_seconds") and usage_remaining <= 0: warnings.warn( "This instance has met its usage limit. Workloads will not run until time is made " - "available. Check https://quantum.cloud.ibm.com/instances for more details." + "available. Check " + f"https://quantum.cloud.ibm.com/instances/{quote(self.active_instance(), safe="")} " + "for more details." ) def _decode_job(self, raw_data: Dict) -> RuntimeJobV2: diff --git a/test/unit/mock/fake_runtime_client.py b/test/unit/mock/fake_runtime_client.py index 43e049a2c..29293bc3e 100644 --- a/test/unit/mock/fake_runtime_client.py +++ b/test/unit/mock/fake_runtime_client.py @@ -463,6 +463,7 @@ def cloud_usage(self) -> Dict[str, Any]: }, "usage_allocation_seconds": 90000, "usage_remaining_seconds": 84000, + "usage_limit_reached": False, } def _find_backend(self, backend_name): diff --git a/test/unit/test_jobs.py b/test/unit/test_jobs.py index b3fe36dea..6e085ba28 100644 --- a/test/unit/test_jobs.py +++ b/test/unit/test_jobs.py @@ -14,6 +14,7 @@ import random import time +from unittest.mock import patch from qiskit.providers.exceptions import QiskitBackendNotFoundError @@ -29,6 +30,7 @@ FailedRuntimeJob, FailedRanTooLongRuntimeJob, CancelableRuntimeJob, + BaseFakeRuntimeClient, ) from ..ibm_test_case import IBMTestCase from ..decorators import run_cloud_fake @@ -155,3 +157,29 @@ def test_delete_job(self, service): service.delete_job(job.job_id()) with self.assertRaises(RuntimeJobNotFound): service.job(job.job_id()) + + @run_cloud_fake + def test_instance_limit_warning(self, service): + """Test running program.""" + instance_usage_msg_1 = { + "usage_consumed_seconds": 6000, + "usage_limit_seconds": 90000, + "usage_remaining_seconds": 84000, + "usage_limit_reached": True, + } + instance_usage_msg_2 = { + "usage_consumed_seconds": 90001, + "usage_limit_seconds": 90000, + "usage_limit_reached": True, + } + with patch.object(BaseFakeRuntimeClient, "cloud_usage", return_value=instance_usage_msg_1): + with self.assertWarns(UserWarning) as cm: + run_program(service=service) + self.assertIn( + "There is currently no more time available", str(cm.warnings[0].message) + ) + + with patch.object(BaseFakeRuntimeClient, "cloud_usage", return_value=instance_usage_msg_2): + with self.assertWarns(UserWarning) as cm: + run_program(service=service) + self.assertIn("This instance has met its usage limit", str(cm.warnings[0].message)) From cc7334dde08df110d4cd9c6add7d736baf35cd1b Mon Sep 17 00:00:00 2001 From: kevin-tian Date: Wed, 29 Oct 2025 15:15:04 -0400 Subject: [PATCH 4/6] lint --- qiskit_ibm_runtime/qiskit_runtime_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_ibm_runtime/qiskit_runtime_service.py b/qiskit_ibm_runtime/qiskit_runtime_service.py index 5e88c5e80..6092fb870 100644 --- a/qiskit_ibm_runtime/qiskit_runtime_service.py +++ b/qiskit_ibm_runtime/qiskit_runtime_service.py @@ -1207,14 +1207,14 @@ def _check_instance_usage(self) -> None: warnings.warn( "There is currently no more time available for this instance’s plan on the account. " "Workloads will not run until time is made available. Check " - f"https://quantum.cloud.ibm.com/instances/{quote(self.active_instance(), safe="")} " + f"https://quantum.cloud.ibm.com/instances/{quote(self.active_instance(), safe='')} " "for more details." ) if usage_dict.get("usage_limit_seconds") and usage_remaining <= 0: warnings.warn( "This instance has met its usage limit. Workloads will not run until time is made " "available. Check " - f"https://quantum.cloud.ibm.com/instances/{quote(self.active_instance(), safe="")} " + f"https://quantum.cloud.ibm.com/instances/{quote(self.active_instance(), safe='')} " "for more details." ) From d8c778b7d428ec48788b5e0b758ca507ce82eb15 Mon Sep 17 00:00:00 2001 From: "Diego M. Rodriguez" Date: Wed, 3 Dec 2025 12:07:37 +0100 Subject: [PATCH 5/6] Update logic based on review comments --- qiskit_ibm_runtime/qiskit_runtime_service.py | 22 ++++++-------- test/unit/test_jobs.py | 31 ++++++++++++-------- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/qiskit_ibm_runtime/qiskit_runtime_service.py b/qiskit_ibm_runtime/qiskit_runtime_service.py index 6092fb870..e09978417 100644 --- a/qiskit_ibm_runtime/qiskit_runtime_service.py +++ b/qiskit_ibm_runtime/qiskit_runtime_service.py @@ -1195,25 +1195,21 @@ def usage(self) -> Dict[str, Any]: return usage_dict def _check_instance_usage(self) -> None: - """Raise warning if instance usage has been reached.""" - usage_dict = self._active_api_client.cloud_usage() - limit_reached = usage_dict.get("usage_limit_reached", False) - usage_remaining = usage_dict.get( - "usage_limit_seconds", usage_dict.get("usage_allocation_seconds") - ) - usage_dict.get("usage_consumed_seconds", 0) + """Emit a warning if instance usage has been reached.""" + usage_dict = self.usage() - if limit_reached: - if not usage_dict.get("usage_limit_seconds") or usage_remaining > 0: + if usage_dict.get("usage_limit_reached"): + if usage_dict.get("usage_limit_seconds") and usage_dict["usage_remaining_seconds"] <= 0: warnings.warn( - "There is currently no more time available for this instance’s plan on the account. " - "Workloads will not run until time is made available. Check " + "This instance has met its usage limit. Workloads will not run until time is made " + "available. Check " f"https://quantum.cloud.ibm.com/instances/{quote(self.active_instance(), safe='')} " "for more details." ) - if usage_dict.get("usage_limit_seconds") and usage_remaining <= 0: + else: warnings.warn( - "This instance has met its usage limit. Workloads will not run until time is made " - "available. Check " + "There is currently no more time available for this instance's plan on the account. " + "Workloads will not run until time is made available. Check " f"https://quantum.cloud.ibm.com/instances/{quote(self.active_instance(), safe='')} " "for more details." ) diff --git a/test/unit/test_jobs.py b/test/unit/test_jobs.py index 6e085ba28..e88b18fe2 100644 --- a/test/unit/test_jobs.py +++ b/test/unit/test_jobs.py @@ -160,26 +160,33 @@ def test_delete_job(self, service): @run_cloud_fake def test_instance_limit_warning(self, service): - """Test running program.""" + """Test emitting a warning if instance usage has been reached.""" + # All relevant fields present, account limit reached. instance_usage_msg_1 = { - "usage_consumed_seconds": 6000, - "usage_limit_seconds": 90000, - "usage_remaining_seconds": 84000, + "usage_consumed_seconds": 1, + "usage_limit_seconds": 2, "usage_limit_reached": True, } + # All relevant fields present, instance limit reached. instance_usage_msg_2 = { - "usage_consumed_seconds": 90001, - "usage_limit_seconds": 90000, + "usage_consumed_seconds": 3, + "usage_limit_seconds": 2, + "usage_limit_reached": True, + } + # Missing `usage_limit_seconds`, account limit reached. + instance_usage_msg_3 = { + "usage_consumed_seconds": 1, "usage_limit_reached": True, } + with patch.object(BaseFakeRuntimeClient, "cloud_usage", return_value=instance_usage_msg_1): - with self.assertWarns(UserWarning) as cm: + with self.assertWarnsRegex(UserWarning, r"There is currently no more time available"): run_program(service=service) - self.assertIn( - "There is currently no more time available", str(cm.warnings[0].message) - ) with patch.object(BaseFakeRuntimeClient, "cloud_usage", return_value=instance_usage_msg_2): - with self.assertWarns(UserWarning) as cm: + with self.assertWarnsRegex(UserWarning, r"This instance has met its usage limit"): + run_program(service=service) + + with patch.object(BaseFakeRuntimeClient, "cloud_usage", return_value=instance_usage_msg_3): + with self.assertWarnsRegex(UserWarning, r"There is currently no more time available"): run_program(service=service) - self.assertIn("This instance has met its usage limit", str(cm.warnings[0].message)) From 7f5cc266aa19d199caf9d89c6204c7db9f8294a0 Mon Sep 17 00:00:00 2001 From: "Diego M. Rodriguez" Date: Fri, 5 Dec 2025 11:07:51 +0100 Subject: [PATCH 6/6] Fix 3.10 annotation --- test/unit/mock/fake_runtime_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/mock/fake_runtime_client.py b/test/unit/mock/fake_runtime_client.py index 2842904a8..e259289f4 100644 --- a/test/unit/mock/fake_runtime_client.py +++ b/test/unit/mock/fake_runtime_client.py @@ -451,7 +451,7 @@ def session_details(self, session_id: str) -> dict[str, Any]: """Return the details of the session.""" return {"id": session_id, "mode": "dedicated", "backend_name": "common_backend"} - def cloud_usage(self) -> Dict[str, Any]: + def cloud_usage(self) -> dict[str, Any]: """Return cloud instance usage information.""" return { "instance_id": "instance_id",