This is an automated email from the ASF dual-hosted git repository.

pierrejeambrun pushed a commit to branch v3-1-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/v3-1-test by this push:
     new 4c95dd677c9 [v3-1-test] Fix slots negative infinity (#61140) (#61768)
4c95dd677c9 is described below

commit 4c95dd677c9d2f764a04ad6b7cd1c5db8c98282e
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Mon Feb 16 17:50:38 2026 +0100

    [v3-1-test] Fix slots negative infinity (#61140) (#61768)
    
    * [v3-1-test] Fix slots negative infinity (#61140)
    
    * added deprecation of project_id parameter
    
    mentioned about deprication of project_id parameter
    
    * Fix Pool API to support unlimited slots -1/infinity
    
    * removed bigquery.rst
    
    * fixed when return inf for json comaptibility
    
    * removed infinity and make sured that only pool slots >=-1(-1 for infinity)
    
    * some reformatting
    
    * in test_post_pool_rejects_infinity_string to post to pools
    
    * pools use -1 as unlimited slots
    
    * Revert "pools use -1 as unlimited slots"
    
    This reverts commit 99f86811bbfcd4b8fa2fa879189618b10547a55a.
    
    * Fix CI
    
    ---------
    (cherry picked from commit 1ceb7ed47b50c83f45ec4bc893fa59b45cb3b69d)
    
    Co-authored-by: kamran Imaz <[email protected]>
    Co-authored-by: kamran imaz <[email protected]>
    Co-authored-by: pierrejeambrun <[email protected]>
    
    * Fix CI
    
    ---------
    
    Co-authored-by: kamran Imaz <[email protected]>
    Co-authored-by: kamran imaz <[email protected]>
    Co-authored-by: pierrejeambrun <[email protected]>
---
 .../api_fastapi/core_api/datamodels/pools.py       | 20 +++++++--
 .../core_api/openapi/v2-rest-api-generated.yaml    |  9 ++--
 .../airflow/ui/openapi-gen/requests/schemas.gen.ts | 13 +++---
 .../airflow/ui/openapi-gen/requests/types.gen.ts   |  6 +++
 .../core_api/routes/public/test_pools.py           | 52 ++++++++++++++++------
 .../src/airflowctl/api/datamodels/generated.py     |  6 +--
 6 files changed, 78 insertions(+), 28 deletions(-)

diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/pools.py 
b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/pools.py
index cde5746b42b..8ebdc99b66e 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/pools.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/pools.py
@@ -20,7 +20,7 @@ from __future__ import annotations
 from collections.abc import Callable
 from typing import Annotated
 
-from pydantic import BeforeValidator, Field, PositiveInt
+from pydantic import BeforeValidator, Field
 
 from airflow.api_fastapi.core_api.base import BaseModel, StrictBaseModel
 
@@ -34,15 +34,27 @@ def _call_function(function: Callable[[], int]) -> int:
     return function()
 
 
+PoolSlots = Annotated[
+    int,
+    Field(ge=-1, description="Number of slots. Use -1 for unlimited."),
+]
+
+
 class BasePool(BaseModel):
     """Base serializer for Pool."""
 
     pool: str = Field(serialization_alias="name")
-    slots: PositiveInt
+    slots: PoolSlots
     description: str | None = Field(default=None)
     include_deferred: bool
 
 
+def _sanitize_open_slots(value) -> int:
+    if isinstance(value, float) and value == float("inf"):
+        return -1
+    return value
+
+
 class PoolResponse(BasePool):
     """Pool serializer for responses."""
 
@@ -50,7 +62,7 @@ class PoolResponse(BasePool):
     running_slots: Annotated[int, BeforeValidator(_call_function)]
     queued_slots: Annotated[int, BeforeValidator(_call_function)]
     scheduled_slots: Annotated[int, BeforeValidator(_call_function)]
-    open_slots: Annotated[int, BeforeValidator(_call_function)]
+    open_slots: Annotated[int, BeforeValidator(lambda v: 
_sanitize_open_slots(_call_function(v)))]
     deferred_slots: Annotated[int, BeforeValidator(_call_function)]
 
 
@@ -65,7 +77,7 @@ class PoolPatchBody(StrictBaseModel):
     """Pool serializer for patch bodies."""
 
     name: str | None = Field(default=None, alias="pool")
-    slots: PositiveInt | None = None
+    slots: PoolSlots | None = None
     description: str | None = None
     include_deferred: bool | None = None
 
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
 
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
index 0bd1bf28dc4..0f4fa84182c 100644
--- 
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
+++ 
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
@@ -11433,8 +11433,9 @@ components:
           title: Name
         slots:
           type: integer
-          exclusiveMinimum: 0.0
+          minimum: -1.0
           title: Slots
+          description: Number of slots. Use -1 for unlimited.
         description:
           anyOf:
           - type: string
@@ -11477,7 +11478,8 @@ components:
         slots:
           anyOf:
           - type: integer
-            exclusiveMinimum: 0.0
+            minimum: -1.0
+            description: Number of slots. Use -1 for unlimited.
           - type: 'null'
           title: Slots
         description:
@@ -11501,8 +11503,9 @@ components:
           title: Name
         slots:
           type: integer
-          exclusiveMinimum: 0.0
+          minimum: -1.0
           title: Slots
+          description: Number of slots. Use -1 for unlimited.
         description:
           anyOf:
           - type: string
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
index 54c6d40dbfe..c87b7a34f75 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
@@ -4248,8 +4248,9 @@ export const $PoolBody = {
         },
         slots: {
             type: 'integer',
-            exclusiveMinimum: 0,
-            title: 'Slots'
+            minimum: -1,
+            title: 'Slots',
+            description: 'Number of slots. Use -1 for unlimited.'
         },
         description: {
             anyOf: [
@@ -4312,7 +4313,8 @@ export const $PoolPatchBody = {
             anyOf: [
                 {
                     type: 'integer',
-                    exclusiveMinimum: 0
+                    minimum: -1,
+                    description: 'Number of slots. Use -1 for unlimited.'
                 },
                 {
                     type: 'null'
@@ -4357,8 +4359,9 @@ export const $PoolResponse = {
         },
         slots: {
             type: 'integer',
-            exclusiveMinimum: 0,
-            title: 'Slots'
+            minimum: -1,
+            title: 'Slots',
+            description: 'Number of slots. Use -1 for unlimited.'
         },
         description: {
             anyOf: [
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
index 726ed514a2b..29e7b3f6031 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
@@ -1129,6 +1129,9 @@ export type PluginResponse = {
  */
 export type PoolBody = {
     name: string;
+    /**
+     * Number of slots. Use -1 for unlimited.
+     */
     slots: number;
     description?: string | null;
     include_deferred?: boolean;
@@ -1157,6 +1160,9 @@ export type PoolPatchBody = {
  */
 export type PoolResponse = {
     name: string;
+    /**
+     * Number of slots. Use -1 for unlimited.
+     */
     slots: number;
     description?: string | null;
     include_deferred: boolean;
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_pools.py 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_pools.py
index 2abd7e23835..48b5dccf0e1 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_pools.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_pools.py
@@ -17,6 +17,7 @@
 from __future__ import annotations
 
 import pytest
+from sqlalchemy import func, select
 
 from airflow.models.pool import Pool
 from airflow.utils.session import provide_session
@@ -266,15 +267,7 @@ class TestPatchPool(TestPoolsEndpoint):
                 {"slots": -10},
                 422,
                 {
-                    "detail": [
-                        {
-                            "ctx": {"gt": 0},
-                            "input": -10,
-                            "loc": ["body", "slots"],
-                            "msg": "Input should be greater than 0",
-                            "type": "greater_than",
-                        },
-                    ],
+                    "detail": "Slots must be greater than or equal to -1. Use 
-1 for unlimited.",
                 },
             ),
             # Partial body on default_pool
@@ -351,10 +344,10 @@ class TestPatchPool(TestPoolsEndpoint):
         body = response.json()
 
         if response.status_code == 422:
-            for error in body["detail"]:
-                # pydantic version can vary in tests (lower constraints), we 
do not assert the url.
-                if "url" in error:
-                    del error["url"]
+            detail = response.json().get("detail")
+            assert detail is not None
+            assert "slots" in str(detail)
+            return
 
         assert body == expected_response
         if response.status_code == 200:
@@ -443,6 +436,39 @@ class TestPostPool(TestPoolsEndpoint):
         assert session.query(Pool).count() == n_pools + 1
         check_last_log(session, dag_id=None, event="post_pool", 
logical_date=None)
 
+    def test_post_pool_allows_unlimited_slots(self, test_client, session):
+        self.create_pools()
+        n_pools = session.scalar(select(func.count()).select_from(Pool))
+
+        response = test_client.post(
+            "/pools",
+            json={
+                "name": "unlimited_pool",
+                "slots": -1,
+                "description": "Unlimited pool",
+                "include_deferred": False,
+            },
+        )
+
+        assert response.status_code == 201
+        body = response.json()
+        assert body["name"] == "unlimited_pool"
+        assert body["slots"] == -1
+        assert body["open_slots"] == -1
+        assert session.scalar(select(func.count()).select_from(Pool)) == 
n_pools + 1
+        check_last_log(session, dag_id=None, event="post_pool", 
logical_date=None)
+
+    def test_post_pool_rejects_infinity_string(self, test_client, session):
+        response = test_client.post(
+            "/pools",
+            json={
+                "name": "bad_pool",
+                "slots": "infinity",
+                "include_deferred": False,
+            },
+        )
+        assert response.status_code == 422
+
     def test_should_respond_401(self, unauthenticated_test_client):
         response = unauthenticated_test_client.post("/pools", json={})
         assert response.status_code == 401
diff --git a/airflow-ctl/src/airflowctl/api/datamodels/generated.py 
b/airflow-ctl/src/airflowctl/api/datamodels/generated.py
index c8f11ec0e60..e5cf62a73e3 100644
--- a/airflow-ctl/src/airflowctl/api/datamodels/generated.py
+++ b/airflow-ctl/src/airflowctl/api/datamodels/generated.py
@@ -614,13 +614,13 @@ class PoolBody(BaseModel):
         extra="forbid",
     )
     name: Annotated[str, Field(max_length=256, title="Name")]
-    slots: Annotated[int, Field(gt=0, title="Slots")]
+    slots: Annotated[int, Field(description="Number of slots. Use -1 for 
unlimited.", ge=-1, title="Slots")]
     description: Annotated[str | None, Field(title="Description")] = None
     include_deferred: Annotated[bool | None, Field(title="Include Deferred")] 
= False
 
 
 class Slots(RootModel[int]):
-    root: Annotated[int, Field(gt=0, title="Slots")]
+    root: Annotated[int, Field(description="Number of slots. Use -1 for 
unlimited.", ge=-1, title="Slots")]
 
 
 class PoolPatchBody(BaseModel):
@@ -643,7 +643,7 @@ class PoolResponse(BaseModel):
     """
 
     name: Annotated[str, Field(title="Name")]
-    slots: Annotated[int, Field(gt=0, title="Slots")]
+    slots: Annotated[int, Field(description="Number of slots. Use -1 for 
unlimited.", ge=-1, title="Slots")]
     description: Annotated[str | None, Field(title="Description")] = None
     include_deferred: Annotated[bool, Field(title="Include Deferred")]
     occupied_slots: Annotated[int, Field(title="Occupied Slots")]

Reply via email to