Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Raise ValueError when requesting more cores than available #489

Merged
merged 2 commits into from
Nov 9, 2024

Conversation

jan-janssen
Copy link
Member

@jan-janssen jan-janssen commented Nov 9, 2024

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced a max_cores parameter for enhanced resource management across various executor classes.
    • Improved task submission logic to validate resource requests against available cores.
  • Bug Fixes

    • Added error handling to raise a ValueError when task submissions exceed the specified core limits.
  • Tests

    • Added a new test to ensure proper error handling when oversubscribing resources.

Copy link
Contributor

coderabbitai bot commented Nov 9, 2024

Walkthrough

The pull request introduces modifications to several classes within the executor library, primarily focusing on the addition of a max_cores parameter. This new parameter is integrated into the constructors of the ExecutorBase, FileExecutor, and ExecutorWithDependencies classes, enhancing their configurability regarding resource management. The submit method in these classes has been updated to include logic for validating core requests against the specified maximum, raising a ValueError if exceeded. Additionally, a new test case is added to ensure proper error handling when resource requests surpass available cores.

Changes

File Change Summary
executorlib/base/executor.py Added max_cores parameter to ExecutorBase constructor; updated submit method to validate cores.
executorlib/cache/executor.py Updated FileExecutor constructor to pass max_cores to ExecutorBase.
executorlib/interactive/executor.py Added max_cores to ExecutorWithDependencies constructor; modified submit and __exit__ methods for task management.
executorlib/interactive/shared.py Added max_cores to constructors of InteractiveExecutor and InteractiveStepExecutor.
tests/test_executor_backend_mpi.py Added test_oversubscribe method to verify ValueError is raised for excessive core requests.

Possibly related PRs

🐇 "In the realm of cores, we now take heed,
With max_cores set, we plant the seed.
Tasks align, no more excess,
A gentle check, to avoid the mess.
In our burrows, we hop with glee,
Resource management, as smooth as can be!" 🐇


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Outside diff range and nitpick comments (11)
tests/test_executor_backend_mpi.py (2)

55-60: LGTM! Test effectively validates core allocation limits.

The test correctly verifies that the executor raises a ValueError when requesting more cores than available. This aligns well with the PR objective.

Consider these minor improvements:

-    def test_oversubscribe(self):
-        with self.assertRaises(ValueError):
-            with Executor(max_cores=1, backend="local", block_allocation=True) as exe:
-                cloudpickle_register(ind=1)
-                fs_1 = exe.submit(calc, 1, resource_dict={"cores": 2})
+    def test_oversubscribe(self):
+        with self.assertRaises(ValueError), \
+             Executor(max_cores=1, backend="local", block_allocation=True) as exe:
+            cloudpickle_register(ind=1)
+            exe.submit(calc, 1, resource_dict={"cores": 2})

Changes:

  1. Combined nested with statements for better readability
  2. Removed unused fs_1 variable since we're only testing for the exception
🧰 Tools
🪛 Ruff

56-57: Use a single with statement with multiple contexts instead of nested with statements

(SIM117)


59-59: Local variable fs_1 is assigned to but never used

Remove assignment to unused variable fs_1

(F841)


55-60: Consider adding more test cases for comprehensive coverage.

While the current test covers the basic oversubscription case, consider adding tests for:

  1. Requesting exactly max_cores (should succeed)
  2. Requesting 0 or negative cores (should raise ValueError)
  3. Edge cases with None or non-integer core values

Here's a suggested implementation:

def test_core_allocation_validation(self):
    # Test exact allocation (should succeed)
    with Executor(max_cores=2, backend="local", block_allocation=True) as exe:
        future = exe.submit(calc, 1, resource_dict={"cores": 2})
        self.assertEqual(future.result(), 1)

    # Test invalid core requests
    invalid_cores = [0, -1, None, "2"]
    for cores in invalid_cores:
        with self.assertRaises(ValueError):
            with Executor(max_cores=2, backend="local", block_allocation=True) as exe:
                exe.submit(calc, 1, resource_dict={"cores": cores})
🧰 Tools
🪛 Ruff

56-57: Use a single with statement with multiple contexts instead of nested with statements

(SIM117)


59-59: Local variable fs_1 is assigned to but never used

Remove assignment to unused variable fs_1

(F841)

executorlib/cache/executor.py (2)

Line range hint 52-60: Add validation for cores in resource_dict

The resource dictionary accepts cores without validation. This could lead to issues with negative, zero, or non-integer values.

Consider adding validation:

         if resource_dict is None:
             resource_dict = {}
+        if "cores" in resource_dict:
+            cores = resource_dict["cores"]
+            if not isinstance(cores, int) or cores <= 0:
+                raise ValueError("cores must be a positive integer")
         resource_dict.update(
             {k: v for k, v in default_resource_dict.items() if k not in resource_dict}
         )

Line range hint 89-126: Critical: max_cores parameter is not propagated to FileExecutor

The create_file_executor function accepts and validates max_cores, but this value isn't passed to the FileExecutor instance. This breaks the core validation chain.

Update the FileExecutor instantiation to include max_cores:

     return FileExecutor(
         cache_directory=cache_directory,
-        resource_dict=resource_dict,
+        resource_dict={**(resource_dict or {}), "cores": max_cores},
         pysqa_config_directory=pysqa_config_directory,
         backend=backend.split("pysqa_")[-1],
         disable_dependencies=disable_dependencies,
     )
executorlib/base/executor.py (3)

21-21: Enhance the max_cores parameter documentation.

The current documentation is too brief. Consider expanding it to include:

  • Parameter type (Optional[int])
  • Default value (None)
  • Validation behavior
  • Error conditions
-        max_cores (int): defines the number cores which can be used in parallel
+        max_cores (Optional[int]): Maximum number of cores that can be used in parallel.
+            If set, raises ValueError when a task requests more cores than this limit.
+            If None (default), no limit is enforced.

24-24: Add validation for negative max_cores values.

While the initialization looks good, consider adding validation to prevent negative values for max_cores.

 def __init__(self, max_cores: Optional[int] = None):
     """
     Initialize the ExecutorBase class.
     """
     cloudpickle_register(ind=3)
+    if max_cores is not None and max_cores <= 0:
+        raise ValueError("max_cores must be a positive integer")
     self._max_cores = max_cores

Also applies to: 29-29


90-98: Enhance cores validation and error handling.

While the validation logic is correct, consider these improvements:

  1. Add type checking for cores value
  2. Provide more informative error message
  3. Consider moving validation to check_resource_dict for consistency
-        cores = resource_dict.get("cores", None)
-        if (
-            cores is not None
-            and self._max_cores is not None
-            and cores > self._max_cores
-        ):
-            raise ValueError(
-                "The specified number of cores is larger than the available number of cores."
-            )
+        cores = resource_dict.get("cores")
+        if cores is not None:
+            if not isinstance(cores, int):
+                raise TypeError("cores must be an integer")
+            if self._max_cores is not None and cores > self._max_cores:
+                raise ValueError(
+                    f"Requested {cores} cores exceeds maximum allowed cores ({self._max_cores})"
+                )
🧰 Tools
🪛 Ruff

90-90: Use resource_dict.get("cores") instead of resource_dict.get("cores", None)

Replace resource_dict.get("cores", None) with resource_dict.get("cores")

(SIM910)

executorlib/interactive/executor.py (2)

Line range hint 29-57: Update docstring to document max_cores parameter.

The class docstring should be updated to include the new max_cores parameter in the Args section.

Add this to the Args section:

        refresh_rate (float, optional): The refresh rate for updating the executor queue. Defaults to 0.01.
        plot_dependency_graph (bool, optional): Whether to generate and plot the dependency graph. Defaults to False.
+       max_cores (Optional[int], optional): Maximum number of cores that can be used. Defaults to None.
        *args: Variable length argument list.
🧰 Tools
🪛 Ruff

63-63: Use kwargs.get("max_cores") instead of kwargs.get("max_cores", None)

Replace kwargs.get("max_cores", None) with kwargs.get("max_cores")

(SIM910)


63-63: Optimize kwargs.get() call.

The explicit None default is redundant as it's the default return value for dict.get().

-        super().__init__(max_cores=kwargs.get("max_cores", None))
+        super().__init__(max_cores=kwargs.get("max_cores"))
🧰 Tools
🪛 Ruff

63-63: Use kwargs.get("max_cores") instead of kwargs.get("max_cores", None)

Replace kwargs.get("max_cores", None) with kwargs.get("max_cores")

(SIM910)

executorlib/interactive/shared.py (2)

134-134: Simplify dictionary access and ensure consistent max_cores handling.

The .get() call can be simplified, and for consistency with InteractiveStepExecutor, consider adding max_cores to executor_kwargs.

-        super().__init__(max_cores=executor_kwargs.get("max_cores", None))
+        super().__init__(max_cores=executor_kwargs.get("max_cores"))
+        executor_kwargs["max_cores"] = executor_kwargs.get("max_cores")
         executor_kwargs["future_queue"] = self._future_queue
🧰 Tools
🪛 Ruff

134-134: Use executor_kwargs.get("max_cores") instead of executor_kwargs.get("max_cores", None)

Replace executor_kwargs.get("max_cores", None) with executor_kwargs.get("max_cores")

(SIM910)


186-186: Simplify dictionary access.

The .get() call includes a redundant default value.

-        super().__init__(max_cores=executor_kwargs.get("max_cores", None))
+        super().__init__(max_cores=executor_kwargs.get("max_cores"))
🧰 Tools
🪛 Ruff

186-186: Use executor_kwargs.get("max_cores") instead of executor_kwargs.get("max_cores", None)

Replace executor_kwargs.get("max_cores", None) with executor_kwargs.get("max_cores")

(SIM910)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between b314e72 and e5331cb.

📒 Files selected for processing (5)
  • executorlib/base/executor.py (2 hunks)
  • executorlib/cache/executor.py (1 hunks)
  • executorlib/interactive/executor.py (1 hunks)
  • executorlib/interactive/shared.py (2 hunks)
  • tests/test_executor_backend_mpi.py (1 hunks)
🧰 Additional context used
🪛 Ruff
executorlib/base/executor.py

90-90: Use resource_dict.get("cores") instead of resource_dict.get("cores", None)

Replace resource_dict.get("cores", None) with resource_dict.get("cores")

(SIM910)

executorlib/interactive/executor.py

63-63: Use kwargs.get("max_cores") instead of kwargs.get("max_cores", None)

Replace kwargs.get("max_cores", None) with kwargs.get("max_cores")

(SIM910)

executorlib/interactive/shared.py

134-134: Use executor_kwargs.get("max_cores") instead of executor_kwargs.get("max_cores", None)

Replace executor_kwargs.get("max_cores", None) with executor_kwargs.get("max_cores")

(SIM910)


186-186: Use executor_kwargs.get("max_cores") instead of executor_kwargs.get("max_cores", None)

Replace executor_kwargs.get("max_cores", None) with executor_kwargs.get("max_cores")

(SIM910)

tests/test_executor_backend_mpi.py

56-57: Use a single with statement with multiple contexts instead of nested with statements

(SIM117)


59-59: Local variable fs_1 is assigned to but never used

Remove assignment to unused variable fs_1

(F841)

🔇 Additional comments (6)
executorlib/cache/executor.py (1)

Line range hint 1-126: Verify FileExecutor usage across the codebase

Let's verify how FileExecutor is used across the codebase to ensure these changes won't cause unexpected issues.

✅ Verification successful

No issues found with FileExecutor changes

The verification shows that the changes to FileExecutor are safe because:

  1. All direct FileExecutor instantiations in tests properly handle the resource_dict with cores parameter
  2. All indirect usages via create_file_executor pass the resource_dict correctly
  3. The default behavior (cores=1) is preserved in the FileExecutor implementation
  4. Resource dictionary usage across the codebase consistently follows the same pattern of specifying cores
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for FileExecutor instantiations and their resource_dict usage
echo "Direct FileExecutor instantiations:"
rg -A 5 "FileExecutor\(" --type python

echo -e "\nIndirect instantiations via create_file_executor:"
rg -A 5 "create_file_executor\(" --type python

echo -e "\nResource dictionary definitions:"
rg "resource_dict.*=.*{.*cores.*:.*" --type python

Length of output: 614


Script:

#!/bin/bash
# Search for FileExecutor instantiations and their resource_dict usage with fixed regex
echo "Direct FileExecutor instantiations:"
rg "FileExecutor\(" -A 5

echo -e "\nIndirect instantiations via create_file_executor:"
rg "create_file_executor\(" -A 5

echo -e "\nResource dictionary definitions:"
rg "resource_dict\s*=\s*\{[^}]*cores" 

Length of output: 6280

executorlib/base/executor.py (1)

99-99: Verify cores validation in check_resource_dict.

The cores validation happens before check_resource_dict. We should verify if check_resource_dict also performs cores validation to avoid redundancy or inconsistency.

✅ Verification successful

No redundant cores validation in check_resource_dict

Based on the search results, check_resource_dict does not perform any cores validation. The cores-related validations are handled separately in check_max_workers_and_cores and validate_number_of_cores functions. Therefore, there is no redundancy or inconsistency in the validation flow.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check if check_resource_dict performs any cores validation

# Search for cores validation in check_resource_dict implementation
rg -A 5 "def check_resource_dict" 
rg "cores" --context 3 "inputcheck.py"

Length of output: 1049


Script:

#!/bin/bash
# Search for cores validation in check_resource_dict with full path
rg "cores" --context 3 "executorlib/standalone/inputcheck.py"

# Get the full implementation of check_resource_dict
ast-grep --pattern 'def check_resource_dict($$$) {
  $$$
}'

Length of output: 967

executorlib/interactive/executor.py (2)

63-63: LGTM! Core validation properly propagated to base class.

The change correctly forwards the max_cores parameter to ExecutorBase for core validation.

🧰 Tools
🪛 Ruff

63-63: Use kwargs.get("max_cores") instead of kwargs.get("max_cores", None)

Replace kwargs.get("max_cores", None) with kwargs.get("max_cores")

(SIM910)


63-63: Verify core validation integration.

Let's verify that the core validation is properly integrated through the inheritance chain.

✅ Verification successful

Based on the shell script results, I can generate the final response:

Core validation integration is properly implemented

The inheritance chain and core validation are correctly integrated:

  • Base class ExecutorBase properly defines and initializes max_cores parameter
  • All executor implementations consistently pass max_cores through super().init()
  • Core validation is centralized in shared utilities and properly used across implementations
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify core validation implementation in base class and its usage

# Test 1: Check base class implementation
echo "Checking ExecutorBase implementation..."
rg -A 5 "class ExecutorBase" executorlib/base/executor.py

# Test 2: Check for other executors that might need similar changes
echo "Checking other executor implementations..."
rg -l "class.*Executor.*\(" 

# Test 3: Check for max_cores parameter usage
echo "Checking max_cores parameter usage..."
rg "max_cores.*=.*" 

Length of output: 6788

🧰 Tools
🪛 Ruff

63-63: Use kwargs.get("max_cores") instead of kwargs.get("max_cores", None)

Replace kwargs.get("max_cores", None) with kwargs.get("max_cores")

(SIM910)

executorlib/interactive/shared.py (2)

Line range hint 379-389: LGTM! Robust resource management implementation.

The _wait_for_free_slots function correctly implements the core requirement validation, ensuring tasks don't exceed the maximum available cores.

🧰 Tools
🪛 Ruff

131-131: Do not use mutable data structures for argument defaults

Replace with None; initialize within function

(B006)


134-134: Use executor_kwargs.get("max_cores") instead of executor_kwargs.get("max_cores", None)

Replace executor_kwargs.get("max_cores", None) with executor_kwargs.get("max_cores")

(SIM910)


134-134: Verify error handling for core oversubscription.

Let's verify that the parent class properly raises ValueError when core limits are exceeded.

Also applies to: 186-186

✅ Verification successful

Core oversubscription error handling is properly implemented

The parent class ExecutorBase correctly implements the core validation by raising a ValueError when the requested cores exceed max_cores. The error message "The specified number of cores is larger than the available number of cores." is clear and descriptive.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the implementation of ValueError in parent class
# Expected: Find the error handling implementation in ExecutorBase

# Search for the parent class implementation
echo "Searching for ExecutorBase implementation..."
rg -A 10 "class ExecutorBase"

# Search for core validation logic
echo "Searching for core validation logic..."
rg -A 5 "raise ValueError.*cores"

Length of output: 861


Script:

#!/bin/bash
# Let's check the complete implementation of ExecutorBase.__init__
ast-grep --pattern 'def __init__(self, max_cores: Optional[int] = None) {
  $$$
}'

# Also search for any validation logic in the class
rg -B 2 -A 5 "max_cores.*=" executorlib/base/executor.py

Length of output: 511


Script:

#!/bin/bash
# Let's check where max_cores is used in the codebase
rg -B 2 -A 5 "self._max_cores" executorlib/

# Also check for any property or method that might use max_cores for validation
ast-grep --pattern 'def $_(self) {
  if self._max_cores $$$:
    $$$
}'

Length of output: 1177

🧰 Tools
🪛 Ruff

134-134: Use executor_kwargs.get("max_cores") instead of executor_kwargs.get("max_cores", None)

Replace executor_kwargs.get("max_cores", None) with executor_kwargs.get("max_cores")

(SIM910)

@@ -48,7 +48,7 @@ def __init__(
backend (str, optional): name of the backend used to spawn tasks.
disable_dependencies (boolean): Disable resolving future objects during the submission.
"""
super().__init__()
super().__init__(max_cores=None)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Critical: Passing None for max_cores bypasses core validation

Setting max_cores=None in the superclass constructor will disable the core validation mechanism, contradicting the PR's objective of raising ValueError when requesting more cores than available.

Consider this fix:

-        super().__init__(max_cores=None)
+        super().__init__(max_cores=resource_dict.get("cores"))

Committable suggestion skipped: line range outside the PR's diff.

@jan-janssen jan-janssen merged commit 90167f7 into main Nov 9, 2024
26 checks passed
@jan-janssen jan-janssen deleted the max_cores branch November 9, 2024 17:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant