|
21 | 21 | ScalingSpec, |
22 | 22 | ServiceConfiguration, |
23 | 23 | TaskConfiguration, |
| 24 | + parse_run_configuration, |
24 | 25 | ) |
25 | 26 | from dstack._internal.core.models.fleets import FleetNodesSpec |
26 | 27 | from dstack._internal.core.models.gateways import GatewayStatus |
|
66 | 67 | create_run, |
67 | 68 | create_user, |
68 | 69 | get_auth_headers, |
| 70 | + get_fleet_configuration, |
69 | 71 | get_fleet_spec, |
70 | 72 | get_instance_offer_with_availability, |
71 | 73 | get_job_provisioning_data, |
@@ -1916,6 +1918,152 @@ async def test_returns_no_offers_if_imported_fleet_specified_without_project_pre |
1916 | 1918 | assert response_json["project_name"] == "importer" |
1917 | 1919 | assert len(response_json["job_plans"][0]["offers"]) == 0 |
1918 | 1920 |
|
| 1921 | + @pytest.mark.asyncio |
| 1922 | + @pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True) |
| 1923 | + @pytest.mark.parametrize( |
| 1924 | + "configuration", |
| 1925 | + [ |
| 1926 | + pytest.param({"type": "dev-environment"}, id="regular-configuration"), |
| 1927 | + pytest.param( |
| 1928 | + {"type": "task", "commands": [":"], "image": "scratch"}, |
| 1929 | + id="special-configuration-used-by-dstack-offer-cli-command", |
| 1930 | + ), |
| 1931 | + pytest.param( |
| 1932 | + {"type": "task", "commands": [":"], "image": "scratch", "fleets": ["test-fleet"]}, |
| 1933 | + id="special-configuration-used-by-dstack-offer-cli-command-with-fleets", # --fleet |
| 1934 | + ), |
| 1935 | + ], |
| 1936 | + ) |
| 1937 | + async def test_preserves_backend_specific_offer_order( |
| 1938 | + self, |
| 1939 | + test_db, |
| 1940 | + session: AsyncSession, |
| 1941 | + client: AsyncClient, |
| 1942 | + configuration: dict, |
| 1943 | + ) -> None: |
| 1944 | + user = await create_user(session=session, global_role=GlobalRole.USER) |
| 1945 | + project = await create_project(session=session, owner=user) |
| 1946 | + await add_project_member( |
| 1947 | + session=session, |
| 1948 | + project=project, |
| 1949 | + user=user, |
| 1950 | + project_role=ProjectRole.USER, |
| 1951 | + ) |
| 1952 | + repo = await create_repo(session=session, project_id=project.id) |
| 1953 | + await create_fleet( |
| 1954 | + session=session, |
| 1955 | + project=project, |
| 1956 | + spec=get_fleet_spec(conf=get_fleet_configuration(name="test-fleet")), |
| 1957 | + ) |
| 1958 | + |
| 1959 | + run_spec = get_run_spec( |
| 1960 | + repo_id=repo.name, configuration=parse_run_configuration(configuration) |
| 1961 | + ) |
| 1962 | + body = {"run_spec": run_spec.dict()} |
| 1963 | + |
| 1964 | + backend_mock_aws = Mock() |
| 1965 | + backend_mock_aws.TYPE = BackendType.AWS |
| 1966 | + backend_mock_aws.compute.return_value.get_offers.return_value = [ |
| 1967 | + get_instance_offer_with_availability(backend=BackendType.AWS, price=1.0), |
| 1968 | + get_instance_offer_with_availability(backend=BackendType.AWS, price=4.0), |
| 1969 | + ] |
| 1970 | + backend_mock_vastai = Mock() |
| 1971 | + backend_mock_vastai.TYPE = BackendType.VASTAI |
| 1972 | + backend_mock_vastai.compute.return_value.get_offers.return_value = [ |
| 1973 | + # not ordered by price - custom order should be preserved |
| 1974 | + get_instance_offer_with_availability(backend=BackendType.VASTAI, price=3.0), |
| 1975 | + get_instance_offer_with_availability(backend=BackendType.VASTAI, price=2.0), |
| 1976 | + ] |
| 1977 | + |
| 1978 | + with patch("dstack._internal.server.services.backends.get_project_backends") as m: |
| 1979 | + m.return_value = [backend_mock_aws, backend_mock_vastai] |
| 1980 | + response = await client.post( |
| 1981 | + f"/api/project/{project.name}/runs/get_plan", |
| 1982 | + headers=get_auth_headers(user.token), |
| 1983 | + json=body, |
| 1984 | + ) |
| 1985 | + |
| 1986 | + assert response.status_code == 200, response.json() |
| 1987 | + offers = [(o["backend"], o["price"]) for o in response.json()["job_plans"][0]["offers"]] |
| 1988 | + expected_offers = [ |
| 1989 | + (BackendType.AWS.value, 1.0), |
| 1990 | + (BackendType.VASTAI.value, 3.0), |
| 1991 | + (BackendType.VASTAI.value, 2.0), |
| 1992 | + (BackendType.AWS.value, 4.0), |
| 1993 | + ] |
| 1994 | + assert offers == expected_offers |
| 1995 | + |
| 1996 | + @pytest.mark.asyncio |
| 1997 | + @pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True) |
| 1998 | + async def test_offer_cli_preserves_backend_specific_offer_order_across_fleets( |
| 1999 | + self, test_db, session: AsyncSession, client: AsyncClient |
| 2000 | + ) -> None: |
| 2001 | + user = await create_user(session=session, global_role=GlobalRole.USER) |
| 2002 | + project = await create_project(session=session, owner=user) |
| 2003 | + await add_project_member( |
| 2004 | + session=session, |
| 2005 | + project=project, |
| 2006 | + user=user, |
| 2007 | + project_role=ProjectRole.USER, |
| 2008 | + ) |
| 2009 | + repo = await create_repo(session=session, project_id=project.id) |
| 2010 | + await create_fleet( |
| 2011 | + session=session, |
| 2012 | + project=project, |
| 2013 | + spec=get_fleet_spec( |
| 2014 | + conf=get_fleet_configuration(name="fleet-aws", backends=[BackendType.AWS]) |
| 2015 | + ), |
| 2016 | + ) |
| 2017 | + await create_fleet( |
| 2018 | + session=session, |
| 2019 | + project=project, |
| 2020 | + spec=get_fleet_spec( |
| 2021 | + conf=get_fleet_configuration(name="fleet-vastai", backends=[BackendType.VASTAI]) |
| 2022 | + ), |
| 2023 | + ) |
| 2024 | + |
| 2025 | + run_spec = get_run_spec( |
| 2026 | + repo_id=repo.name, |
| 2027 | + configuration=TaskConfiguration( |
| 2028 | + commands=[":"], |
| 2029 | + image="scratch", |
| 2030 | + fleets=["fleet-aws", "fleet-vastai"], |
| 2031 | + ), |
| 2032 | + ) |
| 2033 | + body = {"run_spec": run_spec.dict()} |
| 2034 | + |
| 2035 | + backend_mock_aws = Mock() |
| 2036 | + backend_mock_aws.TYPE = BackendType.AWS |
| 2037 | + backend_mock_aws.compute.return_value.get_offers.return_value = [ |
| 2038 | + get_instance_offer_with_availability(backend=BackendType.AWS, price=1.0), |
| 2039 | + get_instance_offer_with_availability(backend=BackendType.AWS, price=4.0), |
| 2040 | + ] |
| 2041 | + backend_mock_vastai = Mock() |
| 2042 | + backend_mock_vastai.TYPE = BackendType.VASTAI |
| 2043 | + backend_mock_vastai.compute.return_value.get_offers.return_value = [ |
| 2044 | + # not ordered by price - custom order should be preserved |
| 2045 | + get_instance_offer_with_availability(backend=BackendType.VASTAI, price=3.0), |
| 2046 | + get_instance_offer_with_availability(backend=BackendType.VASTAI, price=2.0), |
| 2047 | + ] |
| 2048 | + |
| 2049 | + with patch("dstack._internal.server.services.backends.get_project_backends") as m: |
| 2050 | + m.return_value = [backend_mock_aws, backend_mock_vastai] |
| 2051 | + response = await client.post( |
| 2052 | + f"/api/project/{project.name}/runs/get_plan", |
| 2053 | + headers=get_auth_headers(user.token), |
| 2054 | + json=body, |
| 2055 | + ) |
| 2056 | + |
| 2057 | + assert response.status_code == 200, response.json() |
| 2058 | + offers = [(o["backend"], o["price"]) for o in response.json()["job_plans"][0]["offers"]] |
| 2059 | + expected_offers = [ |
| 2060 | + (BackendType.AWS.value, 1.0), |
| 2061 | + (BackendType.VASTAI.value, 3.0), |
| 2062 | + (BackendType.VASTAI.value, 2.0), |
| 2063 | + (BackendType.AWS.value, 4.0), |
| 2064 | + ] |
| 2065 | + assert offers == expected_offers |
| 2066 | + |
1919 | 2067 | @pytest.mark.asyncio |
1920 | 2068 | @pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True) |
1921 | 2069 | async def test_offer_cli_returns_offers_from_all_specified_fleets( |
|
0 commit comments