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
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2024-05-18 - [Fix N+1 query]
**Learning:** Fixed N+1 queries in `rstate` and `onQuery` by bulk fetching data instead of using `rquery` inside a loop.
**Action:** Always check for repeated fetches in loops that can be optimized to a bulk fetch.
27 changes: 23 additions & 4 deletions action_devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,21 @@ def rstate(user_id=None):
if not devices_data:
return {"devices": {"states": {}}}

devices = list(devices_data.keys())
payload = {
"devices": {
"states": {}
}
}
for device in devices:
for device, raw_data in devices_data.items():

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.

medium

The iteration over devices_data.items() will raise an AttributeError if devices_data is not a dictionary. While _get_scoped_snapshot typically returns a dictionary, Firebase can return a list if the keys are sequential integers starting from zero. Since the code now relies on .items(), it is safer to explicitly verify the type before iterating.

Suggested change
for device, raw_data in devices_data.items():
if not isinstance(devices_data, dict):
return payload
for device, raw_data in devices_data.items():

device = str(device)
logger.debug('Getting Device status from: %s', device)
state_data = rquery(device, user_id=user_id)

# Use pre-fetched state if available in snapshot, else fallback
if isinstance(raw_data, dict) and 'states' in raw_data:
state_data = raw_data['states']
else:
state_data = rquery(device, user_id=user_id)

if state_data:
payload['devices']['states'][device] = state_data
logger.debug('Device state: %s', state_data)
Expand Down Expand Up @@ -199,10 +204,24 @@ def onQuery(body, user_id=None):
payload = {
"devices": {},
}

# Optimize: bulk fetch device states to avoid N+1 queries
devices_data = _get_scoped_snapshot(user_id) or {}

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.

medium

Similar to the logic in rstate, ensure devices_data is a dictionary before calling .get(). If _get_scoped_snapshot returns a list, the current implementation will raise an AttributeError and trigger the catch block, returning an empty device list for the entire query.

Suggested change
devices_data = _get_scoped_snapshot(user_id) or {}
snapshot = _get_scoped_snapshot(user_id)
devices_data = snapshot if isinstance(snapshot, dict) else {}


for i in body['inputs']:
for device in i['payload']['devices']:
deviceId = device['id']
data = rquery(deviceId, user_id=user_id)

# Try getting from cache first
data = None
raw_data = devices_data.get(deviceId)
if isinstance(raw_data, dict) and 'states' in raw_data:
data = raw_data['states']

# Fallback to single fetch and validation logic in rquery
if data is None:
data = rquery(deviceId, user_id=user_id)

payload['devices'][deviceId] = data
return payload
except Exception as e:
Expand Down