Repository: apache/cloudstack
Pull request #10712: Backport #9888 to 4.19: Fix Usage inconsistencies
Author: login: winterhazel
Labels: name: component:usage-server
Created: 2025-04-14T19:39:37Z | Merged: 2025-05-09T07:49:14Z

### Description

This PR backports #9888 to 4.19, fixing the issue described in #10687 and some other ones.

- Duplicated entries in the networks helper table, resulting in duplicated usage record generation;
- Port forwarding helper entries are never removed, resulting in removed port forwarding rules generating usage records;
- Load balancer helper entries are never removed, resulting in removed load balancer rules generating usage records;
- VM snapshot helper entries are never removed, resulting in removed VM snapshots generating usage records;
- Helper entries for volumes in the secondary storage are never removed after the volume gets attached to a VM, resulting in secondary storage usage records being generated for volumes that are not in the secondary storage anymore.

I still intend to work on a normalization script before closing #10687, but it can be merged separately from this PR.

### Types of changes

- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] New feature (non-breaking change which adds functionality)
- [X] Bug fix (non-breaking change which fixes an issue)
- [ ] Enhancement (improves an existing feature and functionality)
- [ ] Cleanup (Code refactoring and cleanup, that may add test cases)
- [ ] build/CI
- [ ] test (unit or integration test code)

### Feature/Enhancement Scale or Bug Severity

#### Bug Severity

- [ ] BLOCKER
- [ ] Critical
- [X] Major
- [ ] Minor
- [ ] Trivial

### How Has This Been Tested?

I reproduced the tests described in #9888.


Diff:
diff --git a/engine/schema/src/main/java/com/cloud/usage/dao/UsageNetworksDao.java b/engine/schema/src/main/java/com/cloud/usage/dao/UsageNetworksDao.java
index 9be4c49b55e0..4d84caaad44f 100644
--- a/engine/schema/src/main/java/com/cloud/usage/dao/UsageNetworksDao.java
+++ b/engine/schema/src/main/java/com/cloud/usage/dao/UsageNetworksDao.java
@@ -28,4 +28,6 @@ public interface UsageNetworksDao extends GenericDao<UsageNetworksVO, Long> {
     void remove(long networkId, Date removed);
 
     List<UsageNetworksVO> getUsageRecords(Long accountId, Date startDate, Date endDate);
+
+    List<UsageNetworksVO> listAll(long networkId);
 }
diff --git a/engine/schema/src/main/java/com/cloud/usage/dao/UsageNetworksDaoImpl.java b/engine/schema/src/main/java/com/cloud/usage/dao/UsageNetworksDaoImpl.java
index 8829414f3f26..6b1322289858 100644
--- a/engine/schema/src/main/java/com/cloud/usage/dao/UsageNetworksDaoImpl.java
+++ b/engine/schema/src/main/java/com/cloud/usage/dao/UsageNetworksDaoImpl.java
@@ -16,15 +16,16 @@
 // under the License.
 package com.cloud.usage.dao;
 
-import com.cloud.network.Network;
 import com.cloud.usage.UsageNetworksVO;
 import com.cloud.utils.DateUtil;
 import com.cloud.utils.db.GenericDaoBase;
+import com.cloud.utils.db.SearchBuilder;
 import com.cloud.utils.db.SearchCriteria;
 import com.cloud.utils.db.TransactionLegacy;
 import org.apache.log4j.Logger;
 import org.springframework.stereotype.Component;
 
+import javax.annotation.PostConstruct;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.util.ArrayList;
@@ -39,6 +40,14 @@ public class UsageNetworksDaoImpl extends GenericDaoBase<UsageNetworksVO, Long>
             " account_id = ? AND ((removed IS NULL AND created <= ?) OR (created BETWEEN ? AND ?) OR (removed BETWEEN ? AND ?) " +
             " OR ((created <= ?) AND (removed >= ?)))";
 
+    private SearchBuilder<UsageNetworksVO> usageNetworksSearch;
+
+    @PostConstruct
+    public void init() {
+        usageNetworksSearch = createSearchBuilder();
+        usageNetworksSearch.and("networkId", usageNetworksSearch.entity().getNetworkId(), SearchCriteria.Op.EQ);
+        usageNetworksSearch.done();
+    }
 
     @Override
     public void update(long networkId, long newNetworkOffering, String state) {
@@ -68,11 +77,10 @@ public void remove(long networkId, Date removed) {
             SearchCriteria<UsageNetworksVO> sc = this.createSearchCriteria();
             sc.addAnd("networkId", SearchCriteria.Op.EQ, networkId);
             sc.addAnd("removed", SearchCriteria.Op.NULL);
-            UsageNetworksVO vo = findOneBy(sc);
-            if (vo != null) {
-                vo.setRemoved(removed);
-                vo.setState(Network.State.Destroy.name());
-                update(vo.getId(), vo);
+            List<UsageNetworksVO> usageNetworksVOs = listBy(sc);
+            for (UsageNetworksVO entry : usageNetworksVOs) {
+                entry.setRemoved(removed);
+                update(entry.getId(), entry);
             }
         } catch (final Exception e) {
             txn.rollback();
@@ -131,4 +139,11 @@ public List<UsageNetworksVO> getUsageRecords(Long accountId, Date startDate, Dat
 
         return usageRecords;
     }
+
+    @Override
+    public List<UsageNetworksVO> listAll(long networkId) {
+        SearchCriteria<UsageNetworksVO> sc = usageNetworksSearch.create();
+        sc.setParameters("networkId", networkId);
+        return listBy(sc);
+    }
 }
diff --git a/engine/schema/src/main/java/com/cloud/usage/dao/UsageVpcDao.java b/engine/schema/src/main/java/com/cloud/usage/dao/UsageVpcDao.java
index a1514aba4cad..5167bf88c485 100644
--- a/engine/schema/src/main/java/com/cloud/usage/dao/UsageVpcDao.java
+++ b/engine/schema/src/main/java/com/cloud/usage/dao/UsageVpcDao.java
@@ -24,6 +24,10 @@
 
 public interface UsageVpcDao extends GenericDao<UsageVpcVO, Long> {
     void update(UsageVpcVO usage);
+
     void remove(long vpcId, Date removed);
+
     List<UsageVpcVO> getUsageRecords(Long accountId, Date startDate, Date endDate);
+
+    List<UsageVpcVO> listAll(long vpcId);
 }
diff --git a/engine/schema/src/main/java/com/cloud/usage/dao/UsageVpcDaoImpl.java b/engine/schema/src/main/java/com/cloud/usage/dao/UsageVpcDaoImpl.java
index a42b96486a54..e7ebf706cf3a 100644
--- a/engine/schema/src/main/java/com/cloud/usage/dao/UsageVpcDaoImpl.java
+++ b/engine/schema/src/main/java/com/cloud/usage/dao/UsageVpcDaoImpl.java
@@ -16,15 +16,16 @@
 // under the License.
 package com.cloud.usage.dao;
 
-import com.cloud.network.vpc.Vpc;
 import com.cloud.usage.UsageVpcVO;
 import com.cloud.utils.DateUtil;
 import com.cloud.utils.db.GenericDaoBase;
+import com.cloud.utils.db.SearchBuilder;
 import com.cloud.utils.db.SearchCriteria;
 import com.cloud.utils.db.TransactionLegacy;
 import org.apache.log4j.Logger;
 import org.springframework.stereotype.Component;
 
+import javax.annotation.PostConstruct;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.util.ArrayList;
@@ -39,6 +40,15 @@ public class UsageVpcDaoImpl extends GenericDaoBase<UsageVpcVO, Long> implements
             " account_id = ? AND ((removed IS NULL AND created <= ?) OR (created BETWEEN ? AND ?) OR (removed BETWEEN ? AND ?) " +
             " OR ((created <= ?) AND (removed >= ?)))";
 
+    private SearchBuilder<UsageVpcVO> usageVpcSearch;
+
+    @PostConstruct
+    public void init() {
+        usageVpcSearch = createSearchBuilder();
+        usageVpcSearch.and("vpcId", usageVpcSearch.entity().getVpcId(), SearchCriteria.Op.EQ);
+        usageVpcSearch.done();
+    }
+
     @Override
     public void update(UsageVpcVO usage) {
         TransactionLegacy txn = TransactionLegacy.open(TransactionLegacy.USAGE_DB);
@@ -66,11 +76,10 @@ public void remove(long vpcId, Date removed) {
             SearchCriteria<UsageVpcVO> sc = this.createSearchCriteria();
             sc.addAnd("vpcId", SearchCriteria.Op.EQ, vpcId);
             sc.addAnd("removed", SearchCriteria.Op.NULL);
-            UsageVpcVO vo = findOneBy(sc);
-            if (vo != null) {
-                vo.setRemoved(removed);
-                vo.setState(Vpc.State.Inactive.name());
-                update(vo.getId(), vo);
+            List<UsageVpcVO> usageVpcVOs = listBy(sc);
+            for (UsageVpcVO entry : usageVpcVOs) {
+                entry.setRemoved(removed);
+                update(entry.getId(), entry);
             }
         } catch (final Exception e) {
             txn.rollback();
@@ -128,4 +137,11 @@ public List<UsageVpcVO> getUsageRecords(Long accountId, Date startDate, Date end
 
         return usageRecords;
     }
+
+    @Override
+    public List<UsageVpcVO> listAll(long vpcId) {
+        SearchCriteria<UsageVpcVO> sc = usageVpcSearch.create();
+        sc.setParameters("vpcId", vpcId);
+        return listBy(sc);
+    }
 }
diff --git a/usage/src/main/java/com/cloud/usage/UsageManagerImpl.java b/usage/src/main/java/com/cloud/usage/UsageManagerImpl.java
index 63624cdc3c07..cc129a9ec5e4 100644
--- a/usage/src/main/java/com/cloud/usage/UsageManagerImpl.java
+++ b/usage/src/main/java/com/cloud/usage/UsageManagerImpl.java
@@ -1540,7 +1540,7 @@ private void createVolumeHelperEvent(UsageEventVO event) {
             //For volumes which are 'attached' successfully, set the 'deleted' column in the usage_storage table,
             //so that the secondary storage should stop accounting and only primary will be accounted.
             SearchCriteria<UsageStorageVO> sc = _usageStorageDao.createSearchCriteria();
-            sc.addAnd("id", SearchCriteria.Op.EQ, volId);
+            sc.addAnd("entityId", SearchCriteria.Op.EQ, volId);
             sc.addAnd("storageType", SearchCriteria.Op.EQ, StorageTypes.VOLUME);
             List<UsageStorageVO> volumesVOs = _usageStorageDao.search(sc, null);
             if (volumesVOs != null) {
@@ -1595,7 +1595,8 @@ private void createVolumeHelperEvent(UsageEventVO event) {
             //For Upload event add an entry to the usage_storage table.
             SearchCriteria<UsageStorageVO> sc = _usageStorageDao.createSearchCriteria();
             sc.addAnd("accountId", SearchCriteria.Op.EQ, event.getAccountId());
-            sc.addAnd("id", SearchCriteria.Op.EQ, volId);
+            sc.addAnd("entityId", SearchCriteria.Op.EQ, volId);
+            sc.addAnd("storageType", SearchCriteria.Op.EQ, StorageTypes.VOLUME);
             sc.addAnd("deleted", SearchCriteria.Op.NULL);
             List<UsageStorageVO> volumesVOs = _usageStorageDao.search(sc, null);
 
@@ -1772,7 +1773,7 @@ private void createLoadBalancerHelperEvent(UsageEventVO event) {
         } else if (EventTypes.EVENT_LOAD_BALANCER_DELETE.equals(event.getType())) {
             SearchCriteria<UsageLoadBalancerPolicyVO> sc = _usageLoadBalancerPolicyDao.createSearchCriteria();
             sc.addAnd("accountId", SearchCriteria.Op.EQ, event.getAccountId());
-            sc.addAnd("id", SearchCriteria.Op.EQ, id);
+            sc.addAnd("lbId", SearchCriteria.Op.EQ, id);
             sc.addAnd("deleted", SearchCriteria.Op.NULL);
             List<UsageLoadBalancerPolicyVO> lbVOs = _usageLoadBalancerPolicyDao.search(sc, null);
             if (lbVOs.size() > 1) {
@@ -1806,7 +1807,7 @@ private void createPortForwardingHelperEvent(UsageEventVO event) {
         } else if (EventTypes.EVENT_NET_RULE_DELETE.equals(event.getType())) {
             SearchCriteria<UsagePortForwardingRuleVO> sc = _usagePortForwardingRuleDao.createSearchCriteria();
             sc.addAnd("accountId", SearchCriteria.Op.EQ, event.getAccountId());
-            sc.addAnd("id", SearchCriteria.Op.EQ, id);
+            sc.addAnd("pfId", SearchCriteria.Op.EQ, id);
             sc.addAnd("deleted", SearchCriteria.Op.NULL);
             List<UsagePortForwardingRuleVO> pfVOs = _usagePortForwardingRuleDao.search(sc, null);
             if (pfVOs.size() > 1) {
@@ -2104,7 +2105,7 @@ private void createVmSnapshotOnPrimaryEvent(UsageEventVO event) {
         } else if (EventTypes.EVENT_VM_SNAPSHOT_OFF_PRIMARY.equals(event.getType())) {
             QueryBuilder<UsageSnapshotOnPrimaryVO> sc = QueryBuilder.create(UsageSnapshotOnPrimaryVO.class);
             sc.and(sc.entity().getAccountId(), SearchCriteria.Op.EQ, event.getAccountId());
-            sc.and(sc.entity().getId(), SearchCriteria.Op.EQ, vmId);
+            sc.and(sc.entity().getVmId(), SearchCriteria.Op.EQ, vmId);
             sc.and(sc.entity().getName(), SearchCriteria.Op.EQ, name);
             sc.and(sc.entity().getDeleted(), SearchCriteria.Op.NULL);
             List<UsageSnapshotOnPrimaryVO> vmsnaps = sc.list();
@@ -2142,33 +2143,90 @@ private void createBackupEvent(final UsageEventVO event) {
     }
 
     private void handleNetworkEvent(UsageEventVO event) {
+        String eventType = event.getType();
+        if (EventTypes.EVENT_NETWORK_DELETE.equals(eventType)) {
+            removeNetworkHelperEntry(event);
+        } else if (EventTypes.EVENT_NETWORK_CREATE.equals(eventType)) {
+            createNetworkHelperEntry(event);
+        } else if (EventTypes.EVENT_NETWORK_UPDATE.equals(eventType)) {
+            updateNetworkHelperEntry(event);
+        } else {
+            s_logger.error(String.format("Unknown event type [%s] in Networks event parser. Skipping it.", eventType));
+        }
+    }
+
+    private void removeNetworkHelperEntry(UsageEventVO event) {
+        long networkId = event.getResourceId();
+        s_logger.debug(String.format("Removing helper entries of network [%s].", networkId));
+        usageNetworksDao.remove(networkId, event.getCreateDate());
+    }
+
+    private void createNetworkHelperEntry(UsageEventVO event) {
+        long networkId = event.getResourceId();
         Account account = _accountDao.findByIdIncludingRemoved(event.getAccountId());
         long domainId = account.getDomainId();
-        if (EventTypes.EVENT_NETWORK_DELETE.equals(event.getType())) {
-            usageNetworksDao.remove(event.getResourceId(), event.getCreateDate());
-        } else if (EventTypes.EVENT_NETWORK_CREATE.equals(event.getType())) {
-            UsageNetworksVO usageNetworksVO = new UsageNetworksVO(event.getResourceId(), event.getOfferingId(), event.getZoneId(), event.getAccountId(), domainId, Network.State.Allocated.name(), event.getCreateDate(), null);
-            usageNetworksDao.persist(usageNetworksVO);
-        } else if (EventTypes.EVENT_NETWORK_UPDATE.equals(event.getType())) {
-            usageNetworksDao.update(event.getResourceId(), event.getOfferingId(), event.getResourceType());
-        } else {
-            s_logger.error(String.format("Unknown event type [%s] in Networks event parser. Skipping it.", event.getType()));
+
+        List<UsageNetworksVO> entries = usageNetworksDao.listAll(networkId);
+        if (!entries.isEmpty()) {
+            s_logger.warn(String.format("Received a NETWORK.CREATE event for a network [%s] that already has helper entries; " +
+                    "therefore, we will not create a new one.", networkId));
+            return;
         }
+
+        s_logger.debug(String.format("Creating a helper entry for network [%s].", networkId));
+        UsageNetworksVO usageNetworksVO = new UsageNetworksVO(networkId, event.getOfferingId(), event.getZoneId(),
+                event.getAccountId(), domainId, Network.State.Allocated.name(), event.getCreateDate(), null);
+        usageNetworksDao.persist(usageNetworksVO);
     }
 
-    private void handleVpcEvent(UsageEventVO event) {
+    private void updateNetworkHelperEntry(UsageEventVO event) {
+        long networkId = event.getResourceId();
         Account account = _accountDao.findByIdIncludingRemoved(event.getAccountId());
         long domainId = account.getDomainId();
-        if (EventTypes.EVENT_VPC_DELETE.equals(event.getType())) {
-            usageVpcDao.remove(event.getResourceId(), event.getCreateDate());
-        } else if (EventTypes.EVENT_VPC_CREATE.equals(event.getType())) {
-            UsageVpcVO usageVPCVO = new UsageVpcVO(event.getResourceId(), event.getZoneId(), event.getAccountId(), domainId, Vpc.State.Enabled.name(), event.getCreateDate(), null);
-            usageVpcDao.persist(usageVPCVO);
+
+        s_logger.debug(String.format("Marking previous helper entries of network [%s] as removed.", networkId));
+        usageNetworksDao.remove(networkId, event.getCreateDate());
+
+        s_logger.debug(String.format("Creating an updated helper entry for network [%s].", networkId));
+        UsageNetworksVO usageNetworksVO = new UsageNetworksVO(networkId, event.getOfferingId(), event.getZoneId(),
+                event.getAccountId(), domainId, event.getResourceType(), event.getCreateDate(), null);
+        usageNetworksDao.persist(usageNetworksVO);
+    }
+
+    private void handleVpcEvent(UsageEventVO event) {
+        String eventType = event.getType();
+        if (EventTypes.EVENT_VPC_DELETE.equals(eventType)) {
+            removeVpcHelperEntry(event);
+        } else if (EventTypes.EVENT_VPC_CREATE.equals(eventType)) {
+            createVpcHelperEntry(event);
         } else {
-            s_logger.error(String.format("Unknown event type [%s] in VPC event parser. Skipping it.", event.getType()));
+            s_logger.error(String.format("Unknown event type [%s] in VPC event parser. Skipping it.", eventType));
         }
     }
 
+    private void removeVpcHelperEntry(UsageEventVO event) {
+        long vpcId = event.getResourceId();
+        s_logger.debug(String.format("Removing helper entries of VPC [%s].", vpcId));
+        usageVpcDao.remove(vpcId, event.getCreateDate());
+    }
+
+    private void createVpcHelperEntry(UsageEventVO event) {
+        long vpcId = event.getResourceId();
+        Account account = _accountDao.findByIdIncludingRemoved(event.getAccountId());
+        long domainId = account.getDomainId();
+
+        List<UsageVpcVO> entries = usageVpcDao.listAll(vpcId);
+        if (!entries.isEmpty()) {
+            s_logger.warn(String.format("Active helper entries already exist for VPC [%s]; therefore, we will not create a new one.",
+                    vpcId));
+            return;
+        }
+
+        s_logger.debug(String.format("Creating a helper entry for VPC [%s].", vpcId));
+        UsageVpcVO usageVPCVO = new UsageVpcVO(vpcId, event.getZoneId(), event.getAccountId(), domainId, Vpc.State.Enabled.name(), event.getCreateDate(), null);
+        usageVpcDao.persist(usageVPCVO);
+    }
+
     private class Heartbeat extends ManagedContextRunnable {
         @Override
         protected void runInContext() {


Reviews:
user: login: DaanHoogland
body: some style comments but code looks good!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T07:12:31Z
user: login: rajujith
body: @winterhazel 

In my tests the duplicate usage records are created although the usage events looks fine.

```
mysql> select * from usage_event where  resource_id=204 and type in ('NETWORK.CREATE','NETWORK.DELETE','NETWORK.UPDATE');
+----+----------------+------------+---------------------+---------+-------------+---------------+-------------+-------------+------+---------------+-----------+--------------+
| id | type           | account_id | created             | zone_id | resource_id | resource_name | offering_id | template_id | size | resource_type | processed | virtual_size |
+----+----------------+------------+---------------------+---------+-------------+---------------+-------------+-------------+------+---------------+-----------+--------------+
|  2 | NETWORK.CREATE |          2 | 2025-05-08 05:29:09 |       1 |         204 | net1          |          10 |        NULL | NULL | NULL          |         1 |         NULL |
|  6 | NETWORK.UPDATE |          2 | 2025-05-08 05:31:22 |       1 |         204 | net1          |          10 |        NULL | NULL | Implemented   |         1 |         NULL |
| 13 | NETWORK.UPDATE |          2 | 2025-05-08 05:37:31 |       1 |         204 | net1          |          10 |        NULL | NULL | Allocated     |         1 |         NULL |
| 15 | NETWORK.DELETE |          2 | 2025-05-08 05:37:32 |       1 |         204 | net1          |          10 |        NULL | NULL | NULL          |         1 |         NULL |
+----+----------------+------------+---------------------+---------+-------------+---------------+-------------+-------------+------+---------------+-----------+--------------+
4 rows in set (0.00 sec)

mysql> select account_id,usage_id,usage_type,start_date,end_date from cloud_usage.cloud_usage where usage_id=204;
+------------+----------+------------+---------------------+---------------------+
| account_id | usage_id | usage_type | start_date          | end_date            |
+------------+----------+------------+---------------------+---------------------+
|          2 |      204 |         30 | 2025-05-08 05:00:00 | 2025-05-08 05:59:59 |
|          2 |      204 |         30 | 2025-05-08 05:00:00 | 2025-05-08 05:59:59 |
|          2 |      204 |         30 | 2025-05-08 05:00:00 | 2025-05-08 05:59:59 |
+------------+----------+------------+---------------------+---------------------+
3 rows in set (0.00 sec)
```
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T06:14:44Z
user: login: rajujith
body: LGTM. No more duplicate usage records are observed. 

@winterhazel  yes you are right, I misinterpreted it. 

```
mysql> select account_id,usage_id,usage_type,start_date,end_date,raw_usage,count(*) as count from cloud_usage.cloud_usage where usage_id!=1 and usage_type!=13 group by account_id,usage_id,usage_type,start_date,end_date,raw_usage having count >1;
Empty set (0.00 sec)
```

state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T04:31:38Z

Review comments:
diff_hunk: @@ -2142,15 +2143,29 @@ private void createBackupEvent(final UsageEventVO event) {
     }
 
     private void handleNetworkEvent(UsageEventVO event) {
+        long networkId = event.getResourceId();
         Account account = _accountDao.findByIdIncludingRemoved(event.getAccountId());
         long domainId = account.getDomainId();
         if (EventTypes.EVENT_NETWORK_DELETE.equals(event.getType())) {
-            usageNetworksDao.remove(event.getResourceId(), event.getCreateDate());
+            usageNetworksDao.remove(networkId, event.getCreateDate());
         } else if (EventTypes.EVENT_NETWORK_CREATE.equals(event.getType())) {
-            UsageNetworksVO usageNetworksVO = new UsageNetworksVO(event.getResourceId(), event.getOfferingId(), event.getZoneId(), event.getAccountId(), domainId, Network.State.Allocated.name(), event.getCreateDate(), null);
+            List<UsageNetworksVO> entries = usageNetworksDao.listAll(networkId);
+            if (!entries.isEmpty()) {
+                s_logger.warn(String.format("Received a NETWORK.CREATE event for a network [%s] that already has helper entries; " +
+                                "therefore, we will not create a new one.", networkId));
+                return;
+            }
+            s_logger.debug(String.format("Creating a helper entry for network [%s].", networkId));
+            UsageNetworksVO usageNetworksVO = new UsageNetworksVO(networkId, event.getOfferingId(), event.getZoneId(),
+                    event.getAccountId(), domainId, Network.State.Allocated.name(), event.getCreateDate(), null);
             usageNetworksDao.persist(usageNetworksVO);
path: usage/src/main/java/com/cloud/usage/UsageManagerImpl.java
user: login: DaanHoogland
body: could be a new method (if the return is handled with a checked exception)
created_at: 2025-05-02T07:10:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2142,15 +2143,29 @@ private void createBackupEvent(final UsageEventVO event) {
     }
 
     private void handleNetworkEvent(UsageEventVO event) {
+        long networkId = event.getResourceId();
         Account account = _accountDao.findByIdIncludingRemoved(event.getAccountId());
         long domainId = account.getDomainId();
         if (EventTypes.EVENT_NETWORK_DELETE.equals(event.getType())) {
-            usageNetworksDao.remove(event.getResourceId(), event.getCreateDate());
+            usageNetworksDao.remove(networkId, event.getCreateDate());
         } else if (EventTypes.EVENT_NETWORK_CREATE.equals(event.getType())) {
-            UsageNetworksVO usageNetworksVO = new UsageNetworksVO(event.getResourceId(), event.getOfferingId(), event.getZoneId(), event.getAccountId(), domainId, Network.State.Allocated.name(), event.getCreateDate(), null);
+            List<UsageNetworksVO> entries = usageNetworksDao.listAll(networkId);
+            if (!entries.isEmpty()) {
+                s_logger.warn(String.format("Received a NETWORK.CREATE event for a network [%s] that already has helper entries; " +
+                                "therefore, we will not create a new one.", networkId));
+                return;
+            }
+            s_logger.debug(String.format("Creating a helper entry for network [%s].", networkId));
+            UsageNetworksVO usageNetworksVO = new UsageNetworksVO(networkId, event.getOfferingId(), event.getZoneId(),
+                    event.getAccountId(), domainId, Network.State.Allocated.name(), event.getCreateDate(), null);
             usageNetworksDao.persist(usageNetworksVO);
         } else if (EventTypes.EVENT_NETWORK_UPDATE.equals(event.getType())) {
-            usageNetworksDao.update(event.getResourceId(), event.getOfferingId(), event.getResourceType());
+            s_logger.debug(String.format("Marking previous helper entries of network [%s] as removed.", networkId));
+            usageNetworksDao.remove(networkId, event.getCreateDate());
+            s_logger.debug(String.format("Creating an updated helper entry for network [%s].", networkId));
+            UsageNetworksVO usageNetworksVO = new UsageNetworksVO(networkId, event.getOfferingId(), event.getZoneId(),
+                    event.getAccountId(), domainId, event.getResourceType(), event.getCreateDate(), null);
+            usageNetworksDao.persist(usageNetworksVO);
path: usage/src/main/java/com/cloud/usage/UsageManagerImpl.java
user: login: DaanHoogland
body: could be a new method
created_at: 2025-05-02T07:11:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2162,6 +2177,13 @@ private void handleVpcEvent(UsageEventVO event) {
         if (EventTypes.EVENT_VPC_DELETE.equals(event.getType())) {
             usageVpcDao.remove(event.getResourceId(), event.getCreateDate());
         } else if (EventTypes.EVENT_VPC_CREATE.equals(event.getType())) {
+            List<UsageVpcVO> entries = usageVpcDao.listAll(event.getResourceId());
+            if (!entries.isEmpty()) {
+                s_logger.warn(String.format("Active helper entries already exist for VPC [%s]; therefore, we will not create a new one.",
+                        event.getResourceId()));
+                return;
+            }
+            s_logger.debug(String.format("Creating a helper entry for VPC [%s].", event.getResourceId()));
             UsageVpcVO usageVPCVO = new UsageVpcVO(event.getResourceId(), event.getZoneId(), event.getAccountId(), domainId, Vpc.State.Enabled.name(), event.getCreateDate(), null);
path: usage/src/main/java/com/cloud/usage/UsageManagerImpl.java
user: login: DaanHoogland
body: could be a new method (again, return needs to be handled with a checked exception)
created_at: 2025-05-02T07:12:01Z
author_association: CONTRIBUTOR

PR comments:
user: login: winterhazel
created_at: 2025-04-14T19:39:58Z
body: @blueorangutan package
author_association: MEMBER
user: login: codecov[bot]
created_at: 2025-04-14T19:47:51Z
body: ## [Codecov](https://app.codecov.io/gh/apache/cloudstack/pull/10712?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) Report
Attention: Patch coverage is `0%` with `93 lines` in your changes missing coverage. Please review.
> Project coverage is 15.16%. Comparing base [(`f13cf59`)](https://app.codecov.io/gh/apache/cloudstack/commit/f13cf597a2e00054033ab5851db628680a64f8dc?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) to head [(`4534d7b`)](https://app.codecov.io/gh/apache/cloudstack/commit/4534d7bd59778edd52a9ec5c106fe96639df44d0?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache).
> Report is 25 commits behind head on 4.19.

| [Files with missing lines](https://app.codecov.io/gh/apache/cloudstack/pull/10712?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | Patch % | Lines |
|---|---|---|
| [...rc/main/java/com/cloud/usage/UsageManagerImpl.java](https://app.codecov.io/gh/apache/cloudstack/pull/10712?src=pr&el=tree&filepath=usage%2Fsrc%2Fmain%2Fjava%2Fcom%2Fcloud%2Fusage%2FUsageManagerImpl.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-dXNhZ2Uvc3JjL21haW4vamF2YS9jb20vY2xvdWQvdXNhZ2UvVXNhZ2VNYW5hZ2VySW1wbC5qYXZh) | 0.00% | [65 Missing :warning: ](https://app.codecov.io/gh/apache/cloudstack/pull/10712?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
| [...java/com/cloud/usage/dao/UsageNetworksDaoImpl.java](https://app.codecov.io/gh/apache/cloudstack/pull/10712?src=pr&el=tree&filepath=engine%2Fschema%2Fsrc%2Fmain%2Fjava%2Fcom%2Fcloud%2Fusage%2Fdao%2FUsageNetworksDaoImpl.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-ZW5naW5lL3NjaGVtYS9zcmMvbWFpbi9qYXZhL2NvbS9jbG91ZC91c2FnZS9kYW8vVXNhZ2VOZXR3b3Jrc0Rhb0ltcGwuamF2YQ==) | 0.00% | [14 Missing :warning: ](https://app.codecov.io/gh/apache/cloudstack/pull/10712?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
| [...main/java/com/cloud/usage/dao/UsageVpcDaoImpl.java](https://app.codecov.io/gh/apache/cloudstack/pull/10712?src=pr&el=tree&filepath=engine%2Fschema%2Fsrc%2Fmain%2Fjava%2Fcom%2Fcloud%2Fusage%2Fdao%2FUsageVpcDaoImpl.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-ZW5naW5lL3NjaGVtYS9zcmMvbWFpbi9qYXZhL2NvbS9jbG91ZC91c2FnZS9kYW8vVXNhZ2VWcGNEYW9JbXBsLmphdmE=) | 0.00% | [14 Missing :warning: ](https://app.codecov.io/gh/apache/cloudstack/pull/10712?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff              @@
##               4.19   #10712      +/-   ##
============================================
- Coverage     15.17%   15.16%   -0.01%     
- Complexity    11332    11338       +6     
============================================
  Files          5415     5414       -1     
  Lines        474893   475157     +264     
  Branches      57920    57973      +53     
============================================
+ Hits          72046    72076      +30     
- Misses       394792   395022     +230     
- Partials       8055     8059       +4     
```

| [Flag](https://app.codecov.io/gh/apache/cloudstack/pull/10712/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | Coverage Δ | |
|---|---|---|
| [uitests](https://app.codecov.io/gh/apache/cloudstack/pull/10712/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `4.28% <ø> (+<0.01%)` | :arrow_up: |
| [unittests](https://app.codecov.io/gh/apache/cloudstack/pull/10712/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `15.89% <0.00%> (-0.01%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/apache/cloudstack/pull/10712?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: DaanHoogland
created_at: 2025-04-15T08:05:46Z
body: @winterhazel , can you have a look at https://github.com/apache/cloudstack/issues/10687#issuecomment-2804174489 ?

This backport is fine, but I have my doubt on the overall event model for networks. In short, making the usage server robust is fine, but we also need to fix the event flow.
author_association: CONTRIBUTOR
user: login: DaanHoogland
created_at: 2025-05-01T12:37:55Z
body: @winterhazel do we need more work on this? I think we discussed extra work on other issues/PRs
author_association: CONTRIBUTOR
user: login: DaanHoogland
created_at: 2025-05-01T12:38:09Z
body: @blueorangutan package
author_association: CONTRIBUTOR
user: login: blueorangutan
created_at: 2025-05-01T12:40:03Z
body: @DaanHoogland a [SL] Jenkins job has been kicked to build packages. It will be bundled with  KVM, XenServer and VMware SystemVM templates. I'll keep you posted as I make progress.
author_association: NONE
user: login: blueorangutan
created_at: 2025-05-01T13:37:06Z
body: Packaging result [SF]: ✔️ el8 ✔️ el9 ✔️ debian ✔️ suse15. SL-JID 13234
author_association: NONE
user: login: DaanHoogland
created_at: 2025-05-01T13:40:12Z
body: @blueorangutan test
author_association: CONTRIBUTOR
user: login: blueorangutan
created_at: 2025-05-01T13:42:04Z
body: @DaanHoogland a [SL] Trillian-Jenkins test job (ol8 mgmt + kvm-ol8) has been kicked to run smoke tests
author_association: NONE
user: login: winterhazel
created_at: 2025-05-02T01:50:17Z
body: > @winterhazel do we need more work on this? I think we discussed extra work on other issues/PRs

@DaanHoogland this should be ready now. I made some adjustments here based on the previous discussions:

- when processing a duplicated `NETWORK.CREATE`, ignore it. Previously, we were marking the existing helper entry as removed and creating a new one
- when processing a `NETWORK,UPDATE`, mark the previous entry as removed and create a new one with the updated information. Previously, the existing entry would have its information overridden, which would result in incorrect usage records (the usage records generated for the period before the network was updated would contain the updated information instead of the real information for that period)
author_association: MEMBER
user: login: winterhazel
created_at: 2025-05-02T01:51:19Z
body: @blueorangutan package
author_association: MEMBER
user: login: blueorangutan
created_at: 2025-05-02T01:52:03Z
body: @winterhazel a [SL] Jenkins job has been kicked to build packages. It will be bundled with  KVM, XenServer and VMware SystemVM templates. I'll keep you posted as I make progress.
author_association: NONE
user: login: blueorangutan
created_at: 2025-05-02T02:48:10Z
body: Packaging result [SF]: ✔️ el8 ✔️ el9 ✔️ debian ✔️ suse15. SL-JID 13235
author_association: NONE
user: login: blueorangutan
created_at: 2025-05-02T03:55:17Z
body: <b>[SF] Trillian test result (tid-13192)</b>
Environment: kvm-ol8 (x2), Advanced Networking with Mgmt server ol8
Total time taken: 48497 seconds
Marvin logs: https://github.com/blueorangutan/acs-prs/releases/download/trillian/pr10712-t13192-kvm-ol8.zip
Smoke tests completed. 132 look OK, 1 have errors, 0 did not run
Only failed and skipped tests results shown below:


Test | Result | Time (s) | Test File
--- | --- | --- | ---
test_01_redundant_vpc_site2site_vpn | `Failure` | 426.09 | test_vpc_vpn.py
test_01_vpc_site2site_vpn | `Failure` | 298.38 | test_vpc_vpn.py

author_association: NONE
user: login: DaanHoogland
created_at: 2025-05-02T07:13:03Z
body: @rajujith , can you have a look at this, please?
author_association: CONTRIBUTOR
user: login: winterhazel
created_at: 2025-05-05T02:02:27Z
body: @blueorangutan package
author_association: MEMBER
user: login: blueorangutan
created_at: 2025-05-05T02:04:05Z
body: @winterhazel a [SL] Jenkins job has been kicked to build packages. It will be bundled with  KVM, XenServer and VMware SystemVM templates. I'll keep you posted as I make progress.
author_association: NONE
user: login: blueorangutan
created_at: 2025-05-05T02:59:04Z
body: Packaging result [SF]: ✔️ el8 ✔️ el9 ✔️ debian ✔️ suse15. SL-JID 13252
author_association: NONE
user: login: winterhazel
created_at: 2025-05-09T02:53:09Z
body: @rajujith thanks for testing. These usage records are not duplicated. I think you just misinterpreted the `start_date` and `end_date` values. 

When Usage runs, it splits the total time it has to generate records for into periods defined by the global setting `usage.stats.job.aggregation.range` (by default, this period is of 24 hours). The `start_date` and `end_date` values represent this period.

To interpret the usage records, you also need to check the other columns. I will show some records I got in my environment as an example.

1. First, I performed the same procedure as you of creating a network, implementing it, stopping it and deleting it.

```sql
MariaDB [cloud_usage]> select * from usage_event where resource_id=205 and type in ('NETWORK.CREATE','NETWORK.DELETE','NETWORK.UPDATE');
+----+----------------+------------+---------------------+---------+-------------+---------------+-------------+-------------+------+---------------+-----------+--------------+
| id | type           | account_id | created             | zone_id | resource_id | resource_name | offering_id | template_id | size | resource_type | processed | virtual_size |
+----+----------------+------------+---------------------+---------+-------------+---------------+-------------+-------------+------+---------------+-----------+--------------+
| 18 | NETWORK.CREATE |          2 | 2025-05-09 02:01:40 |       1 |         205 | test-net      |          10 |        NULL | NULL | NULL          |         1 |         NULL |
| 23 | NETWORK.UPDATE |          2 | 2025-05-09 02:02:49 |       1 |         205 | test-net      |          10 |        NULL | NULL | Implemented   |         1 |         NULL |
| 28 | NETWORK.UPDATE |          2 | 2025-05-09 02:05:40 |       1 |         205 | test-net      |          10 |        NULL | NULL | Allocated     |         1 |         NULL |
| 32 | NETWORK.DELETE |          2 | 2025-05-09 02:06:24 |       1 |         205 | test-net      |          10 |        NULL | NULL | NULL          |         1 |         NULL |
+----+----------------+------------+---------------------+---------+-------------+---------------+-------------+-------------+------+---------------+-----------+--------------+
4 rows in set (0.000 sec)
```

2. Then, I ran the Usage job and got the following usage records for that network. Please see the `raw_usage`, `state`, `start_date` and `end_date` values.

```sql
MariaDB [cloud_usage]> select id,usage_type,usage_id,description,raw_usage,state,start_date,end_date from cloud_usage where usage_id = 205;
+------+------------+----------+---------------------------------------------------------+----------------------+-------------+---------------------+---------------------+
| id   | usage_type | usage_id | description                                             | raw_usage            | state       | start_date          | end_date            |
+------+------------+----------+---------------------------------------------------------+----------------------+-------------+---------------------+---------------------+
| 1293 |         30 |      205 | Network usage for network ID: 205, network offering: 10 | 0.019166944548487663 | Allocated   | 2025-05-09 00:00:00 | 2025-05-09 23:59:59 |
| 1295 |         30 |      205 | Network usage for network ID: 205, network offering: 10 |  0.04750027880072594 | Implemented | 2025-05-09 00:00:00 | 2025-05-09 23:59:59 |
| 1296 |         30 |      205 | Network usage for network ID: 205, network offering: 10 | 0.012222499586641788 | Allocated   | 2025-05-09 00:00:00 | 2025-05-09 23:59:59 |
+------+------------+----------+---------------------------------------------------------+----------------------+-------------+---------------------+---------------------+
```

So, the first record does not represent that the network's state was `Allocated` for the full period between `2025-05-09 00:00:00` and `2025-05-09 23:59:59`. Instead, it represents that the network's state was `Allocated` for `0.019166944548487663` hours between `2025-05-09 00:00:00` and `2025-05-09 23:59:59`. We do not know when it transitioned into and out of `Allocated` in the usage record.

Likewise, the second record represents that the network's state was `Implemented` for `0.04750027880072594` hours between `2025-05-09 00:00:00` and `2025-05-09 23:59:59`; the third represents that it was `Allocated` for `0.012222499586641788` more hours during that period.
author_association: MEMBER
user: login: yadvr
created_at: 2025-05-09T04:58:18Z
body: Is this ready for merging @DaanHoogland ?
author_association: MEMBER