From 2cfefda7e4a0e281d0b5a83765d15588aa7bd96c Mon Sep 17 00:00:00 2001 From: Alessandro <37877991+aledefra@users.noreply.github.com> Date: Tue, 7 Jul 2026 15:52:01 +0200 Subject: [PATCH 1/2] Fix Deeploy per-node resource aggregation (#464) * Fix deeploy per-node resource aggregation * Use existing keys in Deeploy resource aggregation * Use direct instance ID constant in resource aggregation * chore: inc ver --- extensions/business/deeploy/deeploy_mixin.py | 59 ++++++++++++---- .../deeploy/tests/test_stack_resources.py | 69 +++++++++++++++++++ ver.py | 2 +- 3 files changed, 116 insertions(+), 14 deletions(-) diff --git a/extensions/business/deeploy/deeploy_mixin.py b/extensions/business/deeploy/deeploy_mixin.py index 6d278d24a..6959dd677 100644 --- a/extensions/business/deeploy/deeploy_mixin.py +++ b/extensions/business/deeploy/deeploy_mixin.py @@ -2442,15 +2442,21 @@ def _aggregate_container_resources(self, inputs): return legacy_resources self.Pd(f"Processing {len(plugins_array)} plugin instances from plugins array") - total_cpu = Decimal("0") - total_memory_mb = 0 - total_storage_mb = 0 - has_storage_resource = False + resource_footprints_by_key = {} + used_instance_ids = set() # Iterate through plugins array (simplified format - each object is an instance) for idx, plugin_instance in enumerate(plugins_array): signature = plugin_instance.get(DEEPLOY_KEYS.PLUGIN_SIGNATURE, "").upper() self.Pd(f"Plugin {idx}: signature={signature}") + instance_id = plugin_instance.get(DEEPLOY_KEYS.PLUGIN_INSTANCE_ID) or plugin_instance.get(ct.CONFIG_INSTANCE.K_INSTANCE_ID) + if instance_id: + instance_id = str(instance_id) + if instance_id in used_instance_ids: + raise ValueError( + f"{DEEPLOY_ERRORS.PLUGINS3}: Duplicate plugin_instance_id '{instance_id}' in plugins array." + ) + used_instance_ids.add(instance_id) # Only aggregate for CONTAINER_APP_RUNNER and WORKER_APP_RUNNER plugins if signature in CONTAINERIZED_APPS_SIGNATURES: @@ -2460,26 +2466,53 @@ def _aggregate_container_resources(self, inputs): container_storage = resources.get(DEEPLOY_RESOURCES.STORAGE) self.Pd(f" Container resources: cpu={cpu}, memory={memory}") - - total_cpu += cpu memory_mb = parse_memory_to_mb(memory) self.Pd(f" Parsed memory: {memory_mb}MB") - total_memory_mb += memory_mb - + storage_mb = 0 + has_storage_resource = False if DEEPLOY_RESOURCES.STORAGE in resources: has_storage_resource = True storage_mb = self._parse_stack_storage_mb(container_storage, context=f"plugin {idx}") self.Pd(f" Container storage: {storage_mb}MB") - total_storage_mb += storage_mb if not is_stack_app: - storage_mb = self._aggregate_fixed_size_volumes_storage_mb(plugin_instance) - if storage_mb > 0: - self.Pd(f" FIXED_SIZE_VOLUMES storage: {storage_mb}MB") - total_storage_mb += storage_mb + fixed_storage_mb = self._aggregate_fixed_size_volumes_storage_mb(plugin_instance) + if fixed_storage_mb > 0: + self.Pd(f" FIXED_SIZE_VOLUMES storage: {fixed_storage_mb}MB") + storage_mb += fixed_storage_mb has_storage_resource = True + + plugin_name = plugin_instance.get(DEEPLOY_KEYS.PLUGIN_NAME) + if instance_id: + resource_key = (signature, DEEPLOY_KEYS.PLUGIN_INSTANCE_ID, str(instance_id)) + elif plugin_name: + resource_key = (signature, DEEPLOY_KEYS.PLUGIN_NAME, str(plugin_name)) + else: + resource_key = (signature, idx) + + footprint = resource_footprints_by_key.get(resource_key) + if footprint is None: + resource_footprints_by_key[resource_key] = (cpu, memory_mb, storage_mb, has_storage_resource) + else: + prev_cpu, prev_memory_mb, prev_storage_mb, prev_has_storage_resource = footprint + resource_footprints_by_key[resource_key] = ( + max(prev_cpu, cpu), + max(prev_memory_mb, memory_mb), + max(prev_storage_mb, storage_mb), + prev_has_storage_resource or has_storage_resource, + ) else: self.Pd(f" Skipping non-container plugin: {signature}") + total_cpu = Decimal("0") + total_memory_mb = 0 + total_storage_mb = 0 + has_storage_resource = False + for cpu, memory_mb, storage_mb, footprint_has_storage_resource in resource_footprints_by_key.values(): + total_cpu += cpu + total_memory_mb += memory_mb + total_storage_mb += storage_mb + has_storage_resource = has_storage_resource or footprint_has_storage_resource + # Return aggregated resources in standard format aggregated = { DEEPLOY_RESOURCES.CPU: self._format_cpu_decimal(total_cpu), diff --git a/extensions/business/deeploy/tests/test_stack_resources.py b/extensions/business/deeploy/tests/test_stack_resources.py index 91b8823ec..c906fa5a5 100644 --- a/extensions/business/deeploy/tests/test_stack_resources.py +++ b/extensions/business/deeploy/tests/test_stack_resources.py @@ -53,6 +53,52 @@ def test_aggregate_container_resources_counts_selected_volume_storage_once(self) self.assertEqual(resources[DEEPLOY_RESOURCES.MEMORY], "1536m") self.assertEqual(resources[DEEPLOY_RESOURCES.STORAGE], "12288m") + def test_aggregate_container_resources_counts_replicated_plugin_name_once_per_node(self): + plugin = make_deeploy_plugin() + inputs = make_inputs( + job_app_type=JOB_APP_TYPES.STACK, + plugins=[ + make_plugin_entry( + "CONTAINER_APP_RUNNER", + plugin_name="container-1", + CONTAINER_RESOURCES={"cpu": 0.5, "memory": "512m", "storage": "4g"}, + ), + make_plugin_entry( + "CONTAINER_APP_RUNNER", + plugin_name="container-1", + CONTAINER_RESOURCES={"cpu": 1, "memory": "1g", "storage": "8g"}, + ), + ], + ) + + resources = plugin._aggregate_container_resources(inputs) + + self.assertEqual(resources[DEEPLOY_RESOURCES.CPU], 1) + self.assertEqual(resources[DEEPLOY_RESOURCES.MEMORY], "1024m") + self.assertEqual(resources[DEEPLOY_RESOURCES.STORAGE], "8192m") + + def test_aggregate_container_resources_keeps_unnamed_occurrences_additive(self): + plugin = make_deeploy_plugin() + inputs = make_inputs( + job_app_type=JOB_APP_TYPES.STACK, + plugins=[ + make_plugin_entry( + "CONTAINER_APP_RUNNER", + CONTAINER_RESOURCES={"cpu": 0.5, "memory": "512m", "storage": "4g"}, + ), + make_plugin_entry( + "CONTAINER_APP_RUNNER", + CONTAINER_RESOURCES={"cpu": 0.5, "memory": "512m", "storage": "4g"}, + ), + ], + ) + + resources = plugin._aggregate_container_resources(inputs) + + self.assertEqual(resources[DEEPLOY_RESOURCES.CPU], 1) + self.assertEqual(resources[DEEPLOY_RESOURCES.MEMORY], "1024m") + self.assertEqual(resources[DEEPLOY_RESOURCES.STORAGE], "8192m") + def test_aggregate_container_resources_uses_two_decimal_cpu_boundaries(self): plugin = make_deeploy_plugin() inputs = make_inputs( @@ -110,6 +156,29 @@ def test_stack_resources_may_fit_under_paid_tier(self): self.assertTrue(plugin.deeploy_check_payment_and_job_owner(inputs, "0xowner", is_create=True)) + def test_generic_duplicate_instance_id_is_rejected_before_paid_tier(self): + plugin = make_deeploy_plugin() + plugin.bc = _FakeBlockchain(job_type=1) # ENTRY: 1 CPU, 2GB RAM, 8GB storage + inputs = make_inputs( + job_id=123, + job_app_type=JOB_APP_TYPES.GENERIC, + plugins=[ + make_plugin_entry( + "CONTAINER_APP_RUNNER", + instance_id="container-1", + CONTAINER_RESOURCES={"cpu": 1, "memory": "2g", "storage": "8g"}, + ), + make_plugin_entry( + "CONTAINER_APP_RUNNER", + instance_id="container-1", + CONTAINER_RESOURCES={"cpu": 1, "memory": "2g", "storage": "8g"}, + ), + ], + ) + + with self.assertRaisesRegex(ValueError, DEEPLOY_ERRORS.PLUGINS3): + plugin.deeploy_check_payment_and_job_owner(inputs, "0xowner", is_create=True) + def test_stack_resources_reject_when_over_paid_tier(self): plugin = make_deeploy_plugin() plugin.bc = _FakeBlockchain(job_type=54) # LITE: 0.5 CPU, 1GB RAM, 4GB storage diff --git a/ver.py b/ver.py index 43a4364e2..269e46c79 100644 --- a/ver.py +++ b/ver.py @@ -1 +1 @@ -__VER__ = '2.10.370' +__VER__ = '2.10.371' From ab2b2c8b507111649d4d08861171d4c494332a8b Mon Sep 17 00:00:00 2001 From: Cristi Bleotiu <164478159+cristibleotiu@users.noreply.github.com> Date: Wed, 8 Jul 2026 12:09:11 +0300 Subject: [PATCH 2/2] chore: inc ver --- ver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ver.py b/ver.py index 269e46c79..aa20a35b5 100644 --- a/ver.py +++ b/ver.py @@ -1 +1 @@ -__VER__ = '2.10.371' +__VER__ = '2.10.380'