Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 36 additions & 15 deletions generic_config_updater/change_applier.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
import importlib
import os
import tempfile
import time
from collections import defaultdict
from swsscommon.swsscommon import ConfigDBConnector
from sonic_py_common import multi_asic
from .gu_common import GenericConfigUpdaterError, genericUpdaterLogging
from .gu_common import get_config_db_as_json
from .gu_common import JsonChange

SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
UPDATER_CONF_FILE = f"{SCRIPT_DIR}/gcu_services_validator.conf.json"
Expand Down Expand Up @@ -64,8 +65,8 @@ class DryRunChangeApplier:
def __init__(self, config_wrapper):
self.config_wrapper = config_wrapper

def apply(self, change):
self.config_wrapper.apply_change_to_config_db(change)
def apply(self, current_configdb: dict, change: JsonChange) -> dict:
return self.config_wrapper.apply_change_to_config_db(current_configdb, change)

def remove_backend_tables_from_config(self, data):
return data
Expand Down Expand Up @@ -137,25 +138,45 @@ def _report_mismatch(self, run_data, upd_data):
log_error("run_data vs expected_data: {}".format(
str(jsondiff.diff(run_data, upd_data))[0:40]))

def apply(self, change):
run_data = get_config_db_as_json(self.scope)
upd_data = prune_empty_table(change.apply(copy.deepcopy(run_data)))
def apply(self, current_configdb: dict, change: JsonChange) -> dict:
run_data = current_configdb
upd_data = prune_empty_table(change.apply(run_data, in_place=False))
upd_keys = defaultdict(dict)

for tbl in sorted(set(run_data.keys()).union(set(upd_data.keys()))):
self._upd_data(tbl, run_data.get(tbl, {}), upd_data.get(tbl, {}), upd_keys)

ret = self._services_validate(run_data, upd_data, upd_keys)
if not ret:
run_data = get_config_db_as_json(self.scope)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why removing this block of code? It seems useful for a bugfix.

Copy link
Copy Markdown
Contributor Author

@bhouse-nexthop bhouse-nexthop Nov 3, 2025

Choose a reason for hiding this comment

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

From the commit message for this change:

ChangeApplier: no need to re-read configdb twice per patch
During change application, the configdb was being read before applying
each patch, then again as a validation step.  Since we are holding
a lock on the configdb and we know the state as we are mutating it,
there is no reason we need to do this as it greatly slows down the
overall application of the patches to Redis.

There is also no reason at all to do the validation step, this appears
to be something left in during development to help find bugs but
not something that will ever catch any sort of issue post-development.
There is still a final validation step that will catch overall errors
that is minimal enough to not cause any performance concerns.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The motivation of #2295 is explained in PR description.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

PR #2295 is about remove_backend_tables_from_config not about re-reading configdb again. When the configdb is infact read again, the remove_backend_tables_from_config is still called.

self.remove_backend_tables_from_config(upd_data)
self.remove_backend_tables_from_config(run_data)
if upd_data != run_data:
self._report_mismatch(run_data, upd_data)
ret = -1
if ret:
# The above function returns 0 on success as it uses shell return codes
if ret != 0:
log_error("Failed to apply Json change")
return ret

# There was a sanity check in this position originally that appeared
# to be development-time code to ensure things were operating correctly.
# It would retrieve the configdb from Redis and perform transformation
# and comparison. Its not possible for the configuration to not be what
# we expect since we have a known state we are mutating with a lock.
# That said we are leaving in the final configuration comparison in
# PatchApplier "just in case".
#
# However, this code did hide a pretty nasty race condition since there
# is no feedback loop for when config_db changes are actually consumed.
# This check would consume high CPU and would take a good amount of
# time (0.5s - 1s).
#
# The below sleep is functionally equivalent in terms of preventing the
# race condition (without the high CPU that might cause other control
# plane issues), but is of course not the proper fix.
#
# An upstream SONiC issue will be opened for the race condition, and
# until resolved leaving this comment in place for future reference.
time.sleep(1)

# Interestingly this function returns the updated data and doesn't
# propagate an error. Maybe it should? Or are exceptions thrown
# from _upd_data on failure? We seem to intentionally only log on
# _services_validate()
return upd_data

def remove_backend_tables_from_config(self, data):
for key in self.backend_tables:
Expand Down
3 changes: 2 additions & 1 deletion generic_config_updater/generic_updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,10 @@ def apply(self, patch, sort=True):
# Apply changes in order
self.logger.log_notice(f"{scope}: applying {changes_len} change{'s' if changes_len != 1 else ''} " \
f"in order{':' if changes_len > 0 else '.'}")
current_config = old_config
for change in changes:
self.logger.log_notice(f" * {change}")
self.changeapplier.apply(change)
current_config = self.changeapplier.apply(current_config, change)

# Validate config updated successfully
self.logger.log_notice(f"{scope}: verifying patch updates are reflected on ConfigDB.")
Expand Down
Loading
Loading