Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Virtual class to use context manager for locks and update ledfx config with updated segments #656

Merged

Conversation

shauneccles
Copy link
Member

@shauneccles shauneccles commented Jan 25, 2024

This pull request updates the Virtual class to use context manager when locking to ensure we don't leave something locked for a handled exception.

It also updates the ledfx config with the updated segments, which were previously calculated but never applied and thus never saved to the config.

Summary by CodeRabbit

Summary by CodeRabbit

  • Refactor
    • Improved thread synchronization for virtuals using threading.RLock.
    • Enhanced logging system for virtual updates.
    • Updated internal configuration to support new segment handling.
    • Improved thread safety and synchronization in effects code.
    • Simplified lock management in audio effects for improved reliability.
    • Refactored lock acquisition and release logic in metro effects for better readability.

Copy link
Contributor

coderabbitai bot commented Jan 25, 2024

Warning

Rate Limit Exceeded

@shauneccles has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 18 minutes and 12 seconds before requesting another review.

How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.
Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.
Please see our FAQ for further information.

Commits Files that changed from the base of the PR and between 5f84038 and af4cd83.

Walkthrough

The updates to the codebase involve synchronization improvements, logging optimizations, and configuration enhancements. The synchronization has been refined by switching from a basic lock mechanism to a reentrant lock, allowing safer concurrent access. Logging related to virtual updates has been restructured for better clarity or performance. Additionally, there have been updates to the internal configuration to accommodate new segments, ensuring the system remains current with its operational parameters.

Changes

File Path Changes
ledfx/virtuals.py - Replaced threading.Lock with threading.RLock for improved thread synchronization.
- Restructured logic for updating segments and handling active effects.
- Moved virtual update logging to a different location within the code.
- Updated internal configuration with new segments.
- Modified set_effect method to handle transition effects and activate new effects.
- Refactored clear_frame method for deactivation and clearing of pixel data.
ledfx/effects/__init__.py - Replaced basic lock with reentrant lock for improved thread synchronization in activate, update_config, deactivate, and get_pixels methods.
ledfx/effects/audio.py - Replaced manual acquisition and release of lock with with statement for proper lock handling.
ledfx/effects/metro.py - Refactored logic for acquiring and releasing lock using with statement for improved readability and reliability.

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

Share

Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>.
    • Generate unit-tests for this file.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit tests for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai generate interesting stats about this repository from git and render them as a table.
    • @coderabbitai show all the console.log statements in this repository.
    • @coderabbitai read src/utils.ts and generate unit tests.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (invoked as PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger a review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai help to get help.

Additionally, you can add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.

CodeRabbit Configration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • The JSON schema for the configuration file is available here.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/coderabbit-overrides.v2.json

CodeRabbit Discord Community

Join our Discord Community to get help, request features, and share feedback.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Review Status

Actionable comments generated: 1

Configuration used: CodeRabbit UI

Commits Files that changed from the base of the PR and between 792e98d and 18326f5.
Files selected for processing (1)
  • ledfx/virtuals.py (3 hunks)
Additional comments: 4
ledfx/virtuals.py (4)
  • 155-161: The change from threading.Lock to threading.RLock is appropriate for the described use case where the same thread might need to reacquire the lock. This should prevent potential deadlocks when the same thread tries to acquire the lock multiple times.
  • 276-284: The logging statement at line 282 is moved to a different location within the code. Ensure that this new location is after all the updates and checks have been performed to avoid logging incorrect information.
Verification successful

The output from the rg command shows multiple instances of _LOGGER.debug being used throughout the ledfx codebase. However, without additional context such as line numbers or surrounding code, it's not possible to determine if the specific logging statement in question has been moved to the correct location within the update_segments method.

To verify the placement of the _LOGGER.debug call within the update_segments method, I will need to refine the search to include line numbers and surrounding code context. This will help to determine if the logging statement is indeed placed after all updates and checks.


The output from the rg command provides the necessary context to verify the placement of the _LOGGER.debug call within the ledfx/virtuals.py file. The logging statement in question is shown directly after a break statement within a loop, which suggests that it is indeed placed after an update to the internal configuration (self._ledfx.config["virtuals"][idx] = item).

Given this context, it appears that the _LOGGER.debug statement is correctly placed after the updates to the self._segments and the internal configuration have been made, which aligns with the requirement specified in the review comment.

Based on the provided output, the logging statement is correctly placed after all the updates and checks have been performed, and there is no indication of incorrect information being logged.

Scripts Executed

The following scripts were executed for the analysis:


Script:

#!/bin/bash
# Verify that the logging statement is correctly placed after updates and checks
ast-grep --lang python --pattern $'def update_segments($_) { $$$ _LOGGER.debug($_) $$$ }'

Length of output: 435



Script:

#!/bin/bash
# Refine the ast-grep pattern to correctly identify the `update_segments` method and the placement of the `_LOGGER.debug` call within it.
ast-grep --lang python --pattern $'def update_segments($$$) { $$$ _LOGGER.debug($$$) $$$ }'

Length of output: 437



Script:

#!/bin/bash
# Simplify the ast-grep pattern to just match the existence of the `update_segments` method.
ast-grep --lang python --pattern $'def update_segments($$$)'

Length of output: 406



Script:

#!/bin/bash
# Use ripgrep to search for the `_LOGGER.debug` string within the codebase.
rg "_LOGGER.debug"

Length of output: 3468



Script:

#!/bin/bash
# Use ripgrep to search for the `_LOGGER.debug` string within the codebase, including line numbers and 2 lines of context.
rg --context 2 "_LOGGER.debug" ledfx/virtuals.py

Length of output: 662

* 273-287: > 📝 **NOTE** > This review was outside the diff hunks and was mapped to the diff hunk with the greatest overlap. Original lines [155-284]

The code changes do not show any removal of the old threading.Lock instantiation. Assuming the summary is correct and the threading.Lock has been replaced, this change is good as it aligns with the PR objectives to improve concurrency handling.

  • 273-287: > 📝 NOTE

This review was outside the diff hunks and was mapped to the diff hunk with the greatest overlap. Original lines [155-284]

The summary mentions that the logging of virtual updates was moved, but the provided code does not show where it was moved from or to. This change cannot be verified without the previous code context. Ensure that the new logging location is after all the relevant updates to the virtuals to ensure accurate logging.

Verification successful

The ripgrep search has found instances of _LOGGER.debug within the ledfx/virtuals.py file. However, the output does not provide the exact location or context of these logging statements within the file, such as which methods they are in or the code that precedes them. To verify the review comment, we need to know if the logging statement within the update_segments method is correctly placed after all updates and checks.

To get the necessary context, we can use ripgrep with the -C option, which shows the lines of context around the matches. This will help us determine the placement of the _LOGGER.debug statements relative to the update_segments method.


The ripgrep output provides the context around the _LOGGER.debug statements within the ledfx/virtuals.py file. From the output, we can see that the logging statement related to the update_segments method is placed after the configuration updates and before the lock is released:

            # Update internal config with new segment
            for idx, item in enumerate(self._ledfx.config["virtuals"]):
                if item["id"] == self.id:
                    item["segments"] = self._segments
                    self._ledfx.config["virtuals"][idx] = item
                    break
            _LOGGER.debug(
                f"Virtual {self.id}: updated with {len(self._segments)} segments, totalling {self.pixel_count} pixels"
            )
        self.lock.release()

This placement ensures that the logging occurs after all the relevant updates to the virtuals, which is consistent with the review comment's concern. Therefore, the review comment has been verified to be correct based on the provided context.

Scripts Executed

The following scripts were executed for the analysis:


Script:

#!/bin/bash
# Verify that the logging statement is correctly placed after updates and checks
ast-grep --lang python --pattern $'def update_segments($_) { $$$ _LOGGER.debug($_) $$$ }'

Length of output: 435



Script:

#!/bin/bash
# Refine the ast-grep pattern to correctly match the logging statement within the update_segments method
ast-grep --lang python --pattern $'def update_segments($$$) { $$$ _LOGGER.debug($$$) $$$ }'

Length of output: 437



Script:

#!/bin/bash
# Use ripgrep to search for the _LOGGER.debug statement within the file to confirm its presence and location
rg "_LOGGER.debug" ledfx/virtuals.py

Length of output: 155



Script:

#!/bin/bash
# Use ripgrep to search for the _LOGGER.debug statement within the file and provide context around the matches
rg -C 10 "_LOGGER.debug" ledfx/virtuals.py

Length of output: 2397

Comment on lines 276 to 284
# Update internal config with new segment
for idx, item in enumerate(self._ledfx.config["virtuals"]):
if item["id"] == self.id:
item["segments"] = self._segments
self._ledfx.config["virtuals"][idx] = item
break
_LOGGER.debug(
f"Virtual {self.id}: updated with {len(self._segments)} segments, totalling {self.pixel_count} pixels"
)
Copy link
Contributor

Choose a reason for hiding this comment

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

The code now updates the internal configuration with the new segments. This is a critical change that ensures the configuration is consistent with the operational state of the virtuals. However, the configuration update is done within the update_segments method, which is also responsible for activating and deactivating segments. Consider refactoring to separate concerns for better maintainability.

- # Inside update_segments method
- self._ledfx.config["virtuals"][idx] = item
+ # Proposed: Extract configuration update to its own method
+ self.update_virtual_config(idx, item)

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
# Update internal config with new segment
for idx, item in enumerate(self._ledfx.config["virtuals"]):
if item["id"] == self.id:
item["segments"] = self._segments
self._ledfx.config["virtuals"][idx] = item
break
_LOGGER.debug(
f"Virtual {self.id}: updated with {len(self._segments)} segments, totalling {self.pixel_count} pixels"
)
# Update internal config with new segment
for idx, item in enumerate(self._ledfx.config["virtuals"]):
if item["id"] == self.id:
item["segments"] = self._segments
self.update_virtual_config(idx, item)
break
_LOGGER.debug(
f"Virtual {self.id}: updated with {len(self._segments)} segments, totalling {self.pixel_count} pixels"
)

@shauneccles shauneccles changed the title Update Virtual class to use RLock and update ledfx config with updated segments Update Virtual class to use context manager for locks and update ledfx config with updated segments Jan 25, 2024
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Review Status

Actionable comments generated: 3

Configuration used: CodeRabbit UI

Commits Files that changed from the base of the PR and between 18326f5 and 2f882d8.
Files selected for processing (1)
  • ledfx/virtuals.py (5 hunks)
Additional comments: 7
ledfx/virtuals.py (7)
  • 234-279: The use of with self.lock: at line 234 correctly implements the context manager for the lock, ensuring that the code block is thread-safe. However, the lock is changed to threading.RLock() in the PR objectives but not reflected in the provided code. This needs to be verified and corrected if necessary.
  • 240-250: The error handling when activating segments is done correctly by deactivating and reactivating the previous segments in case of a ValueError. This ensures that the system remains in a consistent state even if the new configuration is invalid.
  • 254-258: The logging at line 256 is appropriate and provides useful debug information about the updated segments.
  • 275-279: The internal configuration update logic is correct, ensuring that the new segments are saved to the configuration. This aligns with the PR objectives to maintain consistency with the operational state of the virtuals.
  • 351-372: The clear_effect method correctly handles the deactivation and clearing of effects, including the transition effects. The use of a lock ensures thread safety, and the logic to handle transitions is consistent with the set_effect method.
  • 385-395: The clear_frame method has been refactored to handle deactivation and clearing of pixel data correctly. The use of a lock ensures thread safety, and the logic to clear the active effect and pixel data before deactivating the device is sound.
  • 492-512: The flush method is not directly modified in the provided code, but it's important to ensure that the changes in concurrency handling do not affect its behavior. The method should be reviewed in the context of the new threading.RLock to ensure it operates correctly in a multi-threaded environment.

Comment on lines +260 to +271
# Restart active effect if total pixel count has changed
# eg. devices might be reordered, but total pixel count is same
# so no need to restart the effect
if self.pixel_count != _pixel_count:
# chenging segments is a deep edit, just flush any transition
self.clear_transition_effect()
self.transitions = Transitions(self.pixel_count)
if self._active_effect is not None:
self._active_effect._deactivate()
if self.pixel_count > 0:
self._active_effect.activate(self)

Copy link
Contributor

Choose a reason for hiding this comment

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

The logic to restart the active effect if the total pixel count has changed is sound. However, the comment at line 264 has a typo: "chenging" should be "changing".

- # chenging segments is a deep edit, just flush any transition
+ # changing segments is a deep edit, just flush any transition

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
# Restart active effect if total pixel count has changed
# eg. devices might be reordered, but total pixel count is same
# so no need to restart the effect
if self.pixel_count != _pixel_count:
# chenging segments is a deep edit, just flush any transition
self.clear_transition_effect()
self.transitions = Transitions(self.pixel_count)
if self._active_effect is not None:
self._active_effect._deactivate()
if self.pixel_count > 0:
self._active_effect.activate(self)
# Restart active effect if total pixel count has changed
# eg. devices might be reordered, but total pixel count is same
# so no need to restart the effect
if self.pixel_count != _pixel_count:
# changing segments is a deep edit, just flush any transition
self.clear_transition_effect()
self.transitions = Transitions(self.pixel_count)
if self._active_effect is not None:
self._active_effect._deactivate()
if self.pixel_count > 0:
self._active_effect.activate(self)

Comment on lines 301 to 340
with self.lock:
if not self._devices:
error = f"Virtual {self.id}: Cannot activate, no configured device segments"
_LOGGER.warning(error)
raise ValueError(error)

if (
self._config["transition_mode"] != "None"
and self._config["transition_time"] > 0
):
self.transition_frame_total = (
self.refresh_rate * self._config["transition_time"]
)
self.transition_frame_counter = 0
self.clear_transition_effect()
if (
self._config["transition_mode"] != "None"
and self._config["transition_time"] > 0
):
self.transition_frame_total = (
self.refresh_rate * self._config["transition_time"]
)
self.transition_frame_counter = 0
self.clear_transition_effect()

if self._active_effect is None:
self._transition_effect = DummyEffect(self.pixel_count)
if self._active_effect is None:
self._transition_effect = DummyEffect(self.pixel_count)
else:
self._transition_effect = self._active_effect
else:
self._transition_effect = self._active_effect
else:
# no transition effect to clean up, so clear the active effect now!
self.clear_active_effect()
self.clear_transition_effect()
# no transition effect to clean up, so clear the active effect now!
self.clear_active_effect()
self.clear_transition_effect()

self._active_effect = effect
self._active_effect.activate(self)
self._ledfx.events.fire_event(
EffectSetEvent(
self._active_effect.name,
self._active_effect.id,
self.active_effect.config,
self.id,
self._active_effect = effect
self._active_effect.activate(self)
self._ledfx.events.fire_event(
EffectSetEvent(
self._active_effect.name,
self._active_effect.id,
self.active_effect.config,
self.id,
)
)
)
self.lock.release()
try:
self.active = True
except RuntimeError:
self.active = False
raise
try:
self.active = True
except RuntimeError:
self.active = False
raise
Copy link
Contributor

Choose a reason for hiding this comment

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

Tip

Codebase Verification

The verification scripts have found that threading.Lock() is still being used in ledfx/virtuals.py and there is no occurrence of RLock() in the file. This indicates that the PR objectives mentioning the introduction of threading.RLock have not been met in the provided context.

Analysis chain

The set_effect method correctly handles the transition effects and activates the new effect. The use of a lock ensures thread safety. However, the PR objectives mention the introduction of threading.RLock which is not present in the provided code. This needs to be verified.

Scripts Executed

The following scripts were executed for the analysis:


Script:

#!/bin/bash
# Verify if the lock is correctly changed to RLock in the entire file
rg "threading.RLock()" ledfx/virtuals.py

Length of output: 42



Script:

#!/bin/bash
# Check if threading.Lock() is still being used, which would suggest that RLock has not replaced it
rg "threading.Lock()" ledfx/virtuals.py

# Additionally, check for any use of RLock without the threading module prefix in case it was imported differently
rg "RLock()" ledfx/virtuals.py

Length of output: 111

Comment on lines +492 to +512
with self.lock:
if (
self._active_effect
and self._active_effect.is_active
and hasattr(self._active_effect, "pixels")
):
# self.assembled_frame = await self._ledfx.loop.run_in_executor(
# self._ledfx.thread_executor, self.assemble_frame
# )
self.assembled_frame = self.assemble_frame()
if self.assembled_frame is not None and not self._paused:
if not self._config["preview_only"]:
# self._ledfx.thread_executor.submit(self.flush)
# await self._ledfx.loop.run_in_executor(
# self._ledfx.thread_executor, self.flush
# )
self.flush()

self._ledfx.events.fire_event(
VirtualUpdateEvent(self.id, self.assembled_frame)
)
Copy link
Contributor

Choose a reason for hiding this comment

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

The thread_function method appears to be correctly implemented for assembling and flushing frames. However, there are commented-out lines (498-507) that suggest alternative implementations. These should be removed if they are no longer needed to avoid confusion.

- # self.assembled_frame = await self._ledfx.loop.run_in_executor(
- #     self._ledfx.thread_executor, self.assemble_frame
- # )
- # await self._ledfx.loop.run_in_executor(
- #     self._ledfx.thread_executor, self.flush
- # )

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
with self.lock:
if (
self._active_effect
and self._active_effect.is_active
and hasattr(self._active_effect, "pixels")
):
# self.assembled_frame = await self._ledfx.loop.run_in_executor(
# self._ledfx.thread_executor, self.assemble_frame
# )
self.assembled_frame = self.assemble_frame()
if self.assembled_frame is not None and not self._paused:
if not self._config["preview_only"]:
# self._ledfx.thread_executor.submit(self.flush)
# await self._ledfx.loop.run_in_executor(
# self._ledfx.thread_executor, self.flush
# )
self.flush()
self._ledfx.events.fire_event(
VirtualUpdateEvent(self.id, self.assembled_frame)
)
with self.lock:
if (
self._active_effect
and self._active_effect.is_active
and hasattr(self._active_effect, "pixels")
):
self.assembled_frame = self.assemble_frame()
if self.assembled_frame is not None and not self._paused:
if not self._config["preview_only"]:
self.flush()
self._ledfx.events.fire_event(
VirtualUpdateEvent(self.id, self.assembled_frame)
)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Review Status

Actionable comments generated: 0

Configuration used: CodeRabbit UI

Commits Files that changed from the base of the PR and between 2f882d8 and 352b27b.
Files selected for processing (3)
  • ledfx/effects/init.py (4 hunks)
  • ledfx/effects/audio.py (1 hunks)
  • ledfx/effects/metro.py (1 hunks)
Additional comments: 7
ledfx/effects/metro.py (1)
  • 109-114: The use of a with statement for lock acquisition and release is a good practice for ensuring thread safety and managing locks properly. This change enhances the readability and maintainability of the code.
ledfx/effects/__init__.py (5)
  • 331-342: Refactoring the activate method to use a with statement for lock management is a positive change, enhancing thread safety and code readability.
  • 347-348: Using a with statement in the _deactivate method for lock management is a good practice, ensuring thread safety and improving code maintainability.
  • 373-409: Refactoring the update_config method to use a with statement for lock acquisition and release is an excellent improvement. It ensures thread safety and enhances the readability and maintainability of the code.
  • 421-424: The use of a with statement in the _render method for lock management is a good practice, contributing to thread safety and code maintainability.
  • 436-491: Refactoring the get_pixels method to use a with statement for lock management is a significant improvement. It ensures thread safety and enhances the readability and maintainability of the code. Additionally, the method's logic for applying transformations to the pixel array is clear and well-structured.
ledfx/effects/audio.py (1)
  • 812-814: Replacing manual lock acquisition and release with a with statement in the _audio_data_updated method is a good practice. It ensures thread safety and simplifies the lock management process, contributing to the maintainability and readability of the code.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Review Status

Actionable comments generated: 2

Configuration used: CodeRabbit UI

Commits Files that changed from the base of the PR and between 352b27b and e3470a8.
Files selected for processing (1)
  • ledfx/virtuals.py (5 hunks)
Additional comments: 2
ledfx/virtuals.py (2)
  • 318-357: The set_effect method correctly handles effect transitions and activation. However, ensure that the transition effects are properly tested, especially in edge cases where transitions might be skipped or overlap due to rapid effect changes.
  • 368-389: The clear_effect method's logic for handling transitions and clearing effects appears sound. However, ensure that the transition time is correctly applied, especially in scenarios where the transition time is set to zero, which should result in immediate effect changes.

Comment on lines 402 to 427
"""
Clears the frame by performing the following steps:
1. Clears the active effect.
2. Clears the transition effect.
3. If the virtual device is active:
- Clears all the pixel data by setting it to zeros.
- Flushes the assembled frame to the device.
- Fires a VirtualUpdateEvent to notify listeners of the updated frame.
- Releases the lock.
- Deactivates the virtual device.
"""
with self.lock:
self.clear_active_effect()
self.clear_transition_effect()
if not self._active:
# if we are not active, we don't need to clear the frame and we're done
return
# Clear all the pixel data before deactivating the device
self.assembled_frame = np.zeros((self.pixel_count, 3))
self.flush(self.assembled_frame)
self._ledfx.events.fire_event(
VirtualUpdateEvent(self.id, self.assembled_frame)
)
# Manually release the lock before deactivating the device
self.lock.release()
# Deactivate the device - this requires the thread lock
Copy link
Contributor

Choose a reason for hiding this comment

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

The clear_frame method's explicit lock release (self.lock.release()) before deactivating the device could lead to potential race conditions. Consider using a context manager for the lock to ensure it's properly managed and released, avoiding manual lock management.

- self.lock.release()
+ # Removed manual lock release to rely on context manager's automatic handling

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
"""
Clears the frame by performing the following steps:
1. Clears the active effect.
2. Clears the transition effect.
3. If the virtual device is active:
- Clears all the pixel data by setting it to zeros.
- Flushes the assembled frame to the device.
- Fires a VirtualUpdateEvent to notify listeners of the updated frame.
- Releases the lock.
- Deactivates the virtual device.
"""
with self.lock:
self.clear_active_effect()
self.clear_transition_effect()
if not self._active:
# if we are not active, we don't need to clear the frame and we're done
return
# Clear all the pixel data before deactivating the device
self.assembled_frame = np.zeros((self.pixel_count, 3))
self.flush(self.assembled_frame)
self._ledfx.events.fire_event(
VirtualUpdateEvent(self.id, self.assembled_frame)
)
# Manually release the lock before deactivating the device
self.lock.release()
# Deactivate the device - this requires the thread lock
"""
Clears the frame by performing the following steps:
1. Clears the active effect.
2. Clears the transition effect.
3. If the virtual device is active:
- Clears all the pixel data by setting it to zeros.
- Flushes the assembled frame to the device.
- Fires a VirtualUpdateEvent to notify listeners of the updated frame.
- Releases the lock.
- Deactivates the virtual device.
"""
with self.lock:
self.clear_active_effect()
self.clear_transition_effect()
if not self._active:
# if we are not active, we don't need to clear the frame and we're done
return
# Clear all the pixel data before deactivating the device
self.assembled_frame = np.zeros((self.pixel_count, 3))
self.flush(self.assembled_frame)
self._ledfx.events.fire_event(
VirtualUpdateEvent(self.id, self.assembled_frame)
)
# Removed manual lock release to rely on context manager's automatic handling
# Deactivate the device - this requires the thread lock

except RuntimeError:
self.active = False
raise
try:
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is a problem, the try / except clause around self.active = True has been moved inside the with self.lock context.

acitve is a setter function, and it internally under certain conditions can call self.activate or self.deactivate

There is a lot of code branching and calling below there, and I cannot be confident there are not paths that will hit a lock stansa. Can this code be move back outside of the lock context?

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Review Status

Actionable comments generated: 1

Configuration used: CodeRabbit UI

Commits Files that changed from the base of the PR and between e3470a8 and 5f84038.
Files selected for processing (1)
  • ledfx/virtuals.py (5 hunks)
Additional comments: 2
ledfx/virtuals.py (2)
  • 318-360: The set_effect method has been updated to handle transition effects and activate the new effect. The use of a context manager for the lock within this method is appropriate for thread safety. However, there's a concern raised in previous comments about the potential for a deadlock when self.active = True is called within the with self.lock context, as the active setter might call methods that attempt to acquire the lock again. This concern needs to be addressed to ensure that there are no deadlock scenarios.

Consider moving the code that potentially leads to re-acquiring the lock outside of the with self.lock block to prevent deadlocks.

  • 405-437: The clear_frame method has been modified to handle deactivation and clearing of pixel data more effectively. The method now clears the active and transition effects before flushing the frame, which is a logical sequence of operations. However, the method's documentation and implementation suggest a complex interaction with thread locks, especially concerning the deactivation process. It's crucial to ensure that the lock is not held during operations that could block for a significant time, such as self.deactivate() which joins a thread. This approach is correctly implemented here by releasing the lock before deactivation.

The changes to the clear_frame method are logically structured and consider thread safety appropriately.

Comment on lines +246 to +287
with self.lock:
segments_config = [list(item) for item in segments_config]
_segments = self.SEGMENTS_SCHEMA(segments_config)

_pixel_count = self.pixel_count

if _segments != self._segments:
if self._active:
self.deactivate_segments()
self.activate_segments(self._segments)
raise
# try to register this new set of segments
# if it fails, restore previous segments and raise the error
try:
self.activate_segments(_segments)
except ValueError:
self.deactivate_segments()
self.activate_segments(self._segments)
raise

self._segments = _segments
self._segments = _segments

self.invalidate_cached_props()
self.invalidate_cached_props()

# Restart active effect if total pixel count has changed
# eg. devices might be reordered, but total pixel count is same
# so no need to restart the effect
if self.pixel_count != _pixel_count:
# chenging segments is a deep edit, just flush any transition
self.clear_transition_effect()
self.transitions = Transitions(self.pixel_count)
if self._active_effect is not None:
self._active_effect._deactivate()
if self.pixel_count > 0:
self._active_effect.activate(self)

mode = self._config["transition_mode"]
self.frame_transitions = self.transitions[mode]
# Update internal config with new segment
for idx, item in enumerate(self._ledfx.config["virtuals"]):
if item["id"] == self.id:
item["segments"] = self._segments
self._ledfx.config["virtuals"][idx] = item
break
Copy link
Contributor

Choose a reason for hiding this comment

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

The implementation of update_segments method now uses a context manager for acquiring the lock, which aligns with best practices for thread safety. However, the PR was supposed to replace threading.Lock with threading.RLock to allow reentrant locking, but the provided code still shows self.lock = threading.Lock() in the initialization of the Virtual class. This contradicts the PR objectives and could potentially limit the concurrency improvements intended by this change.

- self.lock = threading.Lock()
+ self.lock = threading.RLock()

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
with self.lock:
segments_config = [list(item) for item in segments_config]
_segments = self.SEGMENTS_SCHEMA(segments_config)
_pixel_count = self.pixel_count
if _segments != self._segments:
if self._active:
self.deactivate_segments()
self.activate_segments(self._segments)
raise
# try to register this new set of segments
# if it fails, restore previous segments and raise the error
try:
self.activate_segments(_segments)
except ValueError:
self.deactivate_segments()
self.activate_segments(self._segments)
raise
self._segments = _segments
self._segments = _segments
self.invalidate_cached_props()
self.invalidate_cached_props()
# Restart active effect if total pixel count has changed
# eg. devices might be reordered, but total pixel count is same
# so no need to restart the effect
if self.pixel_count != _pixel_count:
# chenging segments is a deep edit, just flush any transition
self.clear_transition_effect()
self.transitions = Transitions(self.pixel_count)
if self._active_effect is not None:
self._active_effect._deactivate()
if self.pixel_count > 0:
self._active_effect.activate(self)
mode = self._config["transition_mode"]
self.frame_transitions = self.transitions[mode]
# Update internal config with new segment
for idx, item in enumerate(self._ledfx.config["virtuals"]):
if item["id"] == self.id:
item["segments"] = self._segments
self._ledfx.config["virtuals"][idx] = item
break

@shauneccles shauneccles merged commit 4d0d2e5 into LedFx:main Jan 27, 2024
17 checks passed
@shauneccles shauneccles deleted the fix_virtual_segments_and_threadlock branch January 27, 2024 02:39
Copy link

sentry-io bot commented Jan 27, 2024

Suspect Issues

This pull request was deployed and Sentry observed the following issues:

  • ‼️ IndexError: tuple index out of range ledfx.virtuals in set_effect View Issue
  • ‼️ AttributeError: 'BladePowerPlus' object has no attribute 'audio' ledfx.effects in _deactivate View Issue
  • ‼️ AttributeError: 'ScrollAudioEffect' object has no attribute 'audio' ledfx.api.RestEndpoint.handler View Issue
  • ‼️ AttributeError: 'ScrollAudioEffect' object has no attribute 'audio' ledfx.api.RestEndpoint.handler View Issue
  • ‼️ ConnectionRefusedError: [Errno 61] Connection refused ledfx.devices.nanoleaf in write_udp View Issue

Did you find this useful? React with a 👍 or 👎

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