Skip to content

Conversation

@GauravNagesh
Copy link
Contributor

Description

This PR introduces significant performance improvements to the PSU daemon by optimizing Redis database operations through batching. The key change is Batching.

The daemon now uses RedisPipeline, which instead of sending 1 request separately each time, batches multiple Redis request together and sends them. Also flushes any unfilled batches at the end of each monitoring cycle if any requests still remain. This reduces the number of round trips to the Redis database.

Additionally, existing tests were updated to include above change changes and to mock the new RedisPipeline and FieldValuePairs constructors.

Motivation and Context

Why RedisPipeline and Batching :
Currently, the psud daemon creates and maintains a separate db connection internally for each table used and an additional initial connection to state_db as shown here Table Constructor and RedisPipeline Constructor

So in total we maintain 5 separate Redis database connections in psud (1 connection each for CHASSIS_INFO_TABLE, PSU_INFO_TABLE, FAN_INFO_TABLE, PHYSICAL_ENTITY_INFO_TABLE and the initial connection to STATE_DB) which is waste of Redis and system resources. Moreover, since psud purely a single process executing all requests in a sequential manner, we don't need multiple connections. Just one connection shared across all the tables is sufficient.

Also in the existing flow, individual set() calls are made for each device and their attribute. Rather we can collect all device data in batches using RedisPipeline and then do a single bulk update, which will be faster.

We can also optimize database cleanup operations, where we do multiple individual delete operations in __del__ method for each table key across all tables. Rather we can also batch the delete operations using RedisPipeline for faster cleanup during daemon shutdown, which will be quick in signal handling and reboot scenarios.

Overall benefit :

  • Reduced IO network overhead by batching requests
  • Reduced Redis and system system resources by reusing the same connection for RedisPipeline rather than having a seperate connection to Redis for each table

How Has This Been Tested?

The changes were tested on both virtual and physical platforms. Performance benchmarking was conducted to compare the baseline and the updated implementation, capturing relevant metrics across all database operations.

Additional Information (Optional)

@mssonicbld
Copy link
Collaborator

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@GauravNagesh
Copy link
Contributor Author

/azpw run

@mssonicbld
Copy link
Collaborator

/AzurePipelines run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@mssonicbld
Copy link
Collaborator

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@mssonicbld
Copy link
Collaborator

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@vvolam vvolam requested a review from Copilot November 26, 2025 16:21
Copilot finished reviewing on behalf of vvolam November 26, 2025 16:23
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces performance optimizations to the PSU daemon by implementing Redis batching through RedisPipeline. Instead of maintaining separate database connections for each table and sending individual requests, the daemon now uses a shared RedisPipeline instance that batches multiple operations together before flushing them to STATE_DB.

Key changes:

  • Replaced individual Table connections with a shared RedisPipeline instance (batch_size=10)
  • Added periodic pipeline flush at the end of each monitoring cycle with exception handling
  • Updated test mocks to support the new RedisPipeline and buffered Table constructors

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 9 comments.

File Description
sonic-psud/scripts/psud Implemented RedisPipeline for batching database operations across all tables and added flush logic with error handling in the main run loop
sonic-psud/tests/test_DaemonPsud.py Added test coverage for Redis pipeline flush exception scenario
sonic-psud/tests/mocked_libs/swsscommon/swsscommon.py Added RedisPipeline mock class and updated Table mock to support both legacy and buffered constructors

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

if self.first_run:
self.first_run = False

# flush any remaining requests on the pipeline to STAT_DB at end of every cycle
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

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

Typo in comment: "STAT_DB" should be "STATE_DB" to match the actual database name used throughout the codebase.

Suggested change
# flush any remaining requests on the pipeline to STAT_DB at end of every cycle
# flush any remaining requests on the pipeline to STATE_DB at end of every cycle

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

class Table:
def __init__(self, db, table_name):
def __init__(self, db_or_pipeline, table_name, buffered=False):
# Mock to support both both constructors (db, table_name) and (pipeline, table_name, buffered)
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

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

Duplicate word in comment: "both both" should be "both".

Suggested change
# Mock to support both both constructors (db, table_name) and (pipeline, table_name, buffered)
# Mock to support both constructors (db, table_name) and (pipeline, table_name, buffered)

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment on lines 107 to 108
daemon_psud.run()
assert daemon_psud.first_run is False
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

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

The test should verify that statedb_redisPipeline.flush() is called during the successful execution path. Currently, only the exception scenario is explicitly tested. Consider adding an assertion like daemon_psud.statedb_redisPipeline.flush.assert_called() after line 108 to verify the flush operation is invoked in the normal case.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done


class Table:
def __init__(self, db, table_name):
def __init__(self, db_or_pipeline, table_name, buffered=False):
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

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

Extra whitespace before parameter: there are two spaces before db_or_pipeline. Remove one space for consistent formatting.

Suggested change
def __init__(self, db_or_pipeline, table_name, buffered=False):
def __init__(self, db_or_pipeline, table_name, buffered=False):

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment on lines +407 to +410
self.chassis_tbl = swsscommon.Table(self.statedb_redisPipeline, CHASSIS_INFO_TABLE, True)
self.psu_tbl = swsscommon.Table(self.statedb_redisPipeline, PSU_INFO_TABLE, True)
self.fan_tbl = swsscommon.Table(self.statedb_redisPipeline, FAN_INFO_TABLE, True)
self.phy_entity_tbl = swsscommon.Table(self.statedb_redisPipeline, PHYSICAL_ENTITY_INFO_TABLE, True)
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

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

Call to Table.init with too many arguments; should be no more than 2.
Call to Table.init with too many arguments; should be no more than 2.
Call to Table.init with too many arguments; should be no more than 2.
Call to Table.init with too many arguments; should be no more than 2.

Suggested change
self.chassis_tbl = swsscommon.Table(self.statedb_redisPipeline, CHASSIS_INFO_TABLE, True)
self.psu_tbl = swsscommon.Table(self.statedb_redisPipeline, PSU_INFO_TABLE, True)
self.fan_tbl = swsscommon.Table(self.statedb_redisPipeline, FAN_INFO_TABLE, True)
self.phy_entity_tbl = swsscommon.Table(self.statedb_redisPipeline, PHYSICAL_ENTITY_INFO_TABLE, True)
self.chassis_tbl = swsscommon.Table(self.statedb_redisPipeline, CHASSIS_INFO_TABLE)
self.psu_tbl = swsscommon.Table(self.statedb_redisPipeline, PSU_INFO_TABLE)
self.fan_tbl = swsscommon.Table(self.statedb_redisPipeline, FAN_INFO_TABLE)
self.phy_entity_tbl = swsscommon.Table(self.statedb_redisPipeline, PHYSICAL_ENTITY_INFO_TABLE)

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This parameter is needed for the Redis Pipeline implementation and the Table constructor actually does allow these arguments as shown here Table Constructor

Comment on lines +407 to +410
self.chassis_tbl = swsscommon.Table(self.statedb_redisPipeline, CHASSIS_INFO_TABLE, True)
self.psu_tbl = swsscommon.Table(self.statedb_redisPipeline, PSU_INFO_TABLE, True)
self.fan_tbl = swsscommon.Table(self.statedb_redisPipeline, FAN_INFO_TABLE, True)
self.phy_entity_tbl = swsscommon.Table(self.statedb_redisPipeline, PHYSICAL_ENTITY_INFO_TABLE, True)
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

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

Call to Table.init with too many arguments; should be no more than 2.
Call to Table.init with too many arguments; should be no more than 2.
Call to Table.init with too many arguments; should be no more than 2.
Call to Table.init with too many arguments; should be no more than 2.

Suggested change
self.chassis_tbl = swsscommon.Table(self.statedb_redisPipeline, CHASSIS_INFO_TABLE, True)
self.psu_tbl = swsscommon.Table(self.statedb_redisPipeline, PSU_INFO_TABLE, True)
self.fan_tbl = swsscommon.Table(self.statedb_redisPipeline, FAN_INFO_TABLE, True)
self.phy_entity_tbl = swsscommon.Table(self.statedb_redisPipeline, PHYSICAL_ENTITY_INFO_TABLE, True)
self.chassis_tbl = swsscommon.Table(self.statedb_redisPipeline, CHASSIS_INFO_TABLE)
self.psu_tbl = swsscommon.Table(self.statedb_redisPipeline, PSU_INFO_TABLE)
self.fan_tbl = swsscommon.Table(self.statedb_redisPipeline, FAN_INFO_TABLE)
self.phy_entity_tbl = swsscommon.Table(self.statedb_redisPipeline, PHYSICAL_ENTITY_INFO_TABLE)

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This parameter is needed for the Redis Pipeline implementation and the Table constructor actually does allow these arguments as shown here Table Constructor

Comment on lines +407 to +410
self.chassis_tbl = swsscommon.Table(self.statedb_redisPipeline, CHASSIS_INFO_TABLE, True)
self.psu_tbl = swsscommon.Table(self.statedb_redisPipeline, PSU_INFO_TABLE, True)
self.fan_tbl = swsscommon.Table(self.statedb_redisPipeline, FAN_INFO_TABLE, True)
self.phy_entity_tbl = swsscommon.Table(self.statedb_redisPipeline, PHYSICAL_ENTITY_INFO_TABLE, True)
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

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

Call to Table.init with too many arguments; should be no more than 2.
Call to Table.init with too many arguments; should be no more than 2.
Call to Table.init with too many arguments; should be no more than 2.
Call to Table.init with too many arguments; should be no more than 2.

Suggested change
self.chassis_tbl = swsscommon.Table(self.statedb_redisPipeline, CHASSIS_INFO_TABLE, True)
self.psu_tbl = swsscommon.Table(self.statedb_redisPipeline, PSU_INFO_TABLE, True)
self.fan_tbl = swsscommon.Table(self.statedb_redisPipeline, FAN_INFO_TABLE, True)
self.phy_entity_tbl = swsscommon.Table(self.statedb_redisPipeline, PHYSICAL_ENTITY_INFO_TABLE, True)
self.chassis_tbl = swsscommon.Table(self.statedb_redisPipeline, CHASSIS_INFO_TABLE)
self.psu_tbl = swsscommon.Table(self.statedb_redisPipeline, PSU_INFO_TABLE)
self.fan_tbl = swsscommon.Table(self.statedb_redisPipeline, FAN_INFO_TABLE)
self.phy_entity_tbl = swsscommon.Table(self.statedb_redisPipeline, PHYSICAL_ENTITY_INFO_TABLE)

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This parameter is needed for the Redis Pipeline implementation and the Table constructor actually does allow these arguments as shown here Table Constructor

Comment on lines +407 to +410
self.chassis_tbl = swsscommon.Table(self.statedb_redisPipeline, CHASSIS_INFO_TABLE, True)
self.psu_tbl = swsscommon.Table(self.statedb_redisPipeline, PSU_INFO_TABLE, True)
self.fan_tbl = swsscommon.Table(self.statedb_redisPipeline, FAN_INFO_TABLE, True)
self.phy_entity_tbl = swsscommon.Table(self.statedb_redisPipeline, PHYSICAL_ENTITY_INFO_TABLE, True)
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

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

Call to Table.init with too many arguments; should be no more than 2.
Call to Table.init with too many arguments; should be no more than 2.
Call to Table.init with too many arguments; should be no more than 2.
Call to Table.init with too many arguments; should be no more than 2.

Suggested change
self.chassis_tbl = swsscommon.Table(self.statedb_redisPipeline, CHASSIS_INFO_TABLE, True)
self.psu_tbl = swsscommon.Table(self.statedb_redisPipeline, PSU_INFO_TABLE, True)
self.fan_tbl = swsscommon.Table(self.statedb_redisPipeline, FAN_INFO_TABLE, True)
self.phy_entity_tbl = swsscommon.Table(self.statedb_redisPipeline, PHYSICAL_ENTITY_INFO_TABLE, True)
self.chassis_tbl = swsscommon.Table(self.statedb_redisPipeline, CHASSIS_INFO_TABLE)
self.psu_tbl = swsscommon.Table(self.statedb_redisPipeline, PSU_INFO_TABLE)
self.fan_tbl = swsscommon.Table(self.statedb_redisPipeline, FAN_INFO_TABLE)
self.phy_entity_tbl = swsscommon.Table(self.statedb_redisPipeline, PHYSICAL_ENTITY_INFO_TABLE)

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This parameter is needed for the Redis Pipeline implementation and the Table constructor actually does allow these arguments as shown here Table Constructor

def flush(self):
# Mock flush operation - just clear the queue
self.queue.clear()
pass
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

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

Unnecessary 'pass' statement.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@mssonicbld
Copy link
Collaborator

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

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.

2 participants