Skip to content

Update get_history in HAHistory to take a copy of the history of an entity to avoid race conditions#3550

Merged
springfall2008 merged 3 commits intospringfall2008:mainfrom
AndrewColes:main
Mar 15, 2026
Merged

Update get_history in HAHistory to take a copy of the history of an entity to avoid race conditions#3550
springfall2008 merged 3 commits intospringfall2008:mainfrom
AndrewColes:main

Conversation

@AndrewColes
Copy link
Contributor

get_history previously returned (a list containing) the history stored in HAHistory.

This history could then be accessed concurrently with changes being made to it by update_entity, with no locking.

At a minimum, because update_entity sorts the history -- which is not thread safe -- the history may appear transiently empty during these updates.

This race condition caused glitches elsewhere in the code; at a minimum, with load data appearing to be transiently empty, leading to poor estimates.

This commit fixes the issue by making get_history return a copy of the history; and to hold the lock while making this copy, to ensure thread safety.

To militate against the performance impact of this, minute_data in utils.py is updated to take an extra argument -- can_modify_history, default to False. If set to True, this will stop it from taking a copy it would otherwise make of the history object provided to it. fetch.py is then updated to set it to true in some places, as appropriate. Hence the fix has almost no impact on performance.

Fixes #3511 . Tested for two calendar days, see attached screenshot:

image

…ntity, to avoid race conditions.

get_history previously returned (a list containing) the history stored in HAHistory.

This history could then be accessed concurrently with changes being made to it by update_entity, with no locking.

At a minimum, because update_entity sorts the history -- which is not thread safe -- the history may appear transiently empty during these updates.

This race condition caused glitches elsewhere in the code; at a minimum, with load data appearing to be transiently empty, leading to poor estimates.

This commit fixes the issue by making get_history return a *copy* of the history; and to hold the lock while making this copy, to ensure thread safety.

To militate against the performance impact of this, minute_data in utils.py is updated to take an extra argument -- can_modify_history, default to False. If set to True, this will stop it from taking a copy it would otherwise make of the history object provided to it. fetch.py is then updated to set it to true in some places, as appropriate. Hence the fix has almost no impact on performance.

Fixes springfall2008#3511
Copy link
Contributor

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

Improves performance and thread-safety around Home Assistant history handling by avoiding unnecessary deep-copies in minute_data() when safe, while ensuring cached history returned from HAHistory.get_history() cannot be mutated concurrently or by callers.

Changes:

  • Add can_modify_history flag to utils.minute_data() to optionally skip defensive deepcopy().
  • Make HAHistory.get_history() return a deep-copied result to avoid exposing internal cached structures across threads.
  • Opt specific fetch paths into can_modify_history=True where the history object is not reused after conversion.

Reviewed changes

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

File Description
apps/predbat/utils.py Adds can_modify_history parameter to control whether minute_data() deep-copies input history.
apps/predbat/ha.py Deep-copies history results before returning to improve thread-safety and protect cached history from caller mutation.
apps/predbat/fetch.py Passes can_modify_history=True in a few high-traffic conversions where post-call history reuse doesn’t occur.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +110 to 112
result = copy.deepcopy(result)

return result
Comment on lines 315 to 319
max_increment=MAX_INCREMENT,
interpolate=False,
debug=False,
can_modify_history=False,
):
Copy link
Owner

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@springfall2008 springfall2008 merged commit d5f1eb9 into springfall2008:main Mar 15, 2026
1 check passed
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.

Current load glitches to zero a few times a day, in turn reducing forecast load and affecting the plan; it recovers 5 minutes later

3 participants