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

BugFix: Consider only variable and reference classes #104

Closed
wants to merge 6 commits into from

Conversation

kanapuli
Copy link
Contributor

@kanapuli kanapuli commented Jan 14, 2025

Description

  • While browsing the child nodes, the nodeclass type is currently passed as ua.NodeClassVariable | ua.NodeClassObject. This leads to a bug since the code tries to do a bitwise OR operation resulting to the value of 0x03. But if you look at the enums of NodeClass there is no nodeclass with the value of 3. (See the nodeclass image below). This results in an incorrect browsing when BrowseHierarchicalReferences is true

image

Image showing the result of Bitwise OR operation ua.NodeclassVariable | ua.NodeClassObject

image

OPCUA Browser Records with the PR Change

image

OPCUA Browser Records before the Change

image

Summary by CodeRabbit

  • Improvements
    • Enhanced node browsing functionality with detailed logging for better observability.
    • Improved handling and logging of node attributes and references.
    • Added a new method for direct access to the underlying OPC UA node in the interface.
    • Extended mock structure to include a reference to the OPC UA node for testing purposes.

Copy link
Contributor

coderabbitai bot commented Jan 14, 2025

Walkthrough

The pull request introduces extensive modifications to the OPC UA browsing functionality in the browse.go file, focusing on enhancing logging and refining the handling of node attributes and references. Key updates include improved logging for the browse function and its related methods, as well as the addition of a new method in the NodeBrowser interface for accessing the underlying opcua.Node. The mock wrapper for unit tests has also been updated to include a reference to an opcua.Node, enhancing its functionality.

Changes

File Change Summary
opcua_plugin/browse.go - Enhanced logging in browse, browseChildrenV2, browseChildren, and browseReferencesDeprecated functions.
- Refined handling of node attributes and references, including logging for nil or unreadable attributes.
opcua_plugin/opcua_node_wrapper.go - Added Node() *opcua.Node method to NodeBrowser interface and implemented it in OpcuaNodeWrapper.
opcua_plugin/opcua_unittest_test.go - Added node *opcua.Node field and Node() method to MockOpcuaNodeWraper struct.

Possibly related PRs

Suggested reviewers

  • led0nk
  • JeremyTheocharis
  • Scarjit

Poem

🐰 In the forest of nodes, we now explore,
With logs that guide us, we’ll find even more.
Attributes shining, references clear,
A rabbit's delight, as we hop without fear!
Through OPC UA, our journey's begun,
Each node a treasure, each browse a fun run! 🌟


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3a44b77 and d518492.

📒 Files selected for processing (1)
  • opcua_plugin/browse.go (13 hunks)
🧰 Additional context used
🪛 GitHub Actions: sensorconnect
opcua_plugin/browse.go

[error] 313-313: Nil pointer dereference in browse function when attempting to access Node.Attribute

🪛 GitHub Actions: tag-processor
opcua_plugin/browse.go

[error] 313-313: Invalid memory access in browse function causing segmentation violation

🪛 GitHub Actions: s7-plc
opcua_plugin/browse.go

[error] 313-313: Invalid memory access in browse function causing segmentation fault

🪛 GitHub Actions: nodered-js
opcua_plugin/browse.go

[error] 313-313: Invalid memory access in browse function causing segmentation violation

🪛 GitHub Actions: opcua-plc
opcua_plugin/browse.go

[error] 313-313: Invalid memory access in browse function causing segmentation violation


[error] Ginkgo test suite timed out waiting for all parallel processes to report back

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: release
🔇 Additional comments (6)
opcua_plugin/browse.go (6)

98-101: LGTM! Enhanced logging improves debugging capabilities.

The added debug logging statements provide valuable context about the browse function's execution and node namespace information.

🧰 Tools
🪛 GitHub Actions: opcua-plc

[error] Ginkgo test suite timed out waiting for all parallel processes to report back


Line range hint 124-141: LGTM! Improved error handling with detailed logging.

The enhanced error handling and logging provide better visibility into attribute processing failures.

🧰 Tools
🪛 GitHub Actions: opcua-plc

[error] Ginkgo test suite timed out waiting for all parallel processes to report back


147-166: LGTM! Enhanced NodeClass processing with proper fallback handling.

The added logging provides clear visibility into NodeClass determination and fallback behavior.

🧰 Tools
🪛 GitHub Actions: opcua-plc

[error] Ginkgo test suite timed out waiting for all parallel processes to report back


322-338: LGTM! Improved reference node handling with better separation of concerns.

The separate handling of variable and object class references with detailed logging improves code clarity and debugging capabilities.

🧰 Tools
🪛 GitHub Actions: opcua-plc

[error] Ginkgo test suite timed out waiting for all parallel processes to report back


343-360: LGTM! Well-structured implementation of browseChildrenV2.

The implementation properly separates variable and object class children handling with comprehensive logging.

🧰 Tools
🪛 GitHub Actions: opcua-plc

[error] Ginkgo test suite timed out waiting for all parallel processes to report back


Line range hint 630-709: LGTM! Standardized error handling with improved logging.

The error handling has been standardized across the codebase with comprehensive logging for better visibility into failures.

🧰 Tools
🪛 GitHub Actions: opcua-plc

[error] Ginkgo test suite timed out waiting for all parallel processes to report back

Finishing Touches

  • 📝 Generate Docstrings (Beta)

🪧 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>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • 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 testing code 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 gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

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 using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • 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/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@kanapuli kanapuli marked this pull request as ready for review January 14, 2025 15:16
@kanapuli kanapuli self-assigned this Jan 14, 2025
@kanapuli kanapuli requested a review from led0nk January 14, 2025 15:17
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.

Actionable comments posted: 0

🧹 Nitpick comments (3)
opcua_plugin/browse.go (3)

284-296: Consider logging errors when fetching referenced nodes

In the hasNodeReferencedComponents function, if an error occurs when fetching variable or object class references, the error is silently ignored by returning false. This could make debugging more difficult. Consider logging the errors to assist with troubleshooting and provide more visibility into potential issues.

Apply this diff to add error logging:

 func() bool {
     variableClassRefs, err := n.ReferencedNodes(ctx, id.HasComponent, ua.BrowseDirectionForward, ua.NodeClassVariable, true)
     if err != nil {
+        logger.Warnf("Failed to fetch variable class references: %v", err)
         return false
     }
     objectClassRefs, err := n.ReferencedNodes(ctx, id.HasComponent, ua.BrowseDirectionForward, ua.NodeClassObject, true)
     if err != nil {
+        logger.Warnf("Failed to fetch object class references: %v", err)
         return false
     }
     if len(variableClassRefs) == 0 && len(objectClassRefs) == 0 {
         return false
     }
     return true
 }

301-315: Refactor to reduce code duplication in browseChildrenV2

The browseChildrenV2 function contains duplicated code when fetching variable and object class children. To improve maintainability and readability, consider creating a helper function to fetch and append children based on the node class.

Here's how you can refactor the code:

 func browseChildrenV2(refType uint32) error {
     children := make([]NodeBrowser, 0)
-    variableClassChildren, err := n.Children(ctx, refType, ua.NodeClassVariable)
+    nodeClasses := []ua.NodeClass{ua.NodeClassVariable, ua.NodeClassObject}
+    for _, nodeClass := range nodeClasses {
+        classChildren, err := n.Children(ctx, refType, nodeClass)
     if err != nil {
         sendError(ctx, errors.Errorf("Children: %d: %s", refType, err), errChan, logger)
         return err
     }
-    children = append(children, variableClassChildren...)
-    objectClassChildren, err := n.Children(ctx, refType, ua.NodeClassObject)
-    if err != nil {
-        sendError(ctx, errors.Errorf("Children: %d: %s", refType, err), errChan, logger)
-        return err
-    }
-    children = append(children, objectClassChildren...)
+        children = append(children, classChildren...)
+    }
     for _, child := range children {
         wg.Add(1)
         go browse(ctx, child, def.Path, level+1, logger, def.NodeID.String(), nodeChan, errChan, wg, browseHierarchicalReferences, opcuaBrowserChan)
     }
     return nil
 }

324-337: Refactor duplicated logic in browseChildren function

Similar to browseChildrenV2, the browseChildren function has duplicated code for fetching variable and object class references. Refactoring this code can enhance maintainability and reduce potential errors due to code duplication.

Here's a suggested refactor:

 func browseChildren(refType uint32) error {
     refs := make([]NodeBrowser, 0)
-    variableClassReferences, err := n.ReferencedNodes(ctx, refType, ua.BrowseDirectionForward, ua.NodeClassVariable, true)
-    if err != nil {
-        sendError(ctx, errors.Errorf("References: %d: %s", refType, err), errChan, logger)
-        return err
-    }
-    refs = append(refs, variableClassReferences...)
-    objectClassReferences, err := n.ReferencedNodes(ctx, refType, ua.BrowseDirectionForward, ua.NodeClassObject, true)
-    if err != nil {
-        sendError(ctx, errors.Errorf("References: %d: %s", refType, err), errChan, logger)
-        return err
-    }
-    refs = append(refs, objectClassReferences...)
+    nodeClasses := []ua.NodeClass{ua.NodeClassVariable, ua.NodeClassObject}
+    for _, nodeClass := range nodeClasses {
+        classReferences, err := n.ReferencedNodes(ctx, refType, ua.BrowseDirectionForward, nodeClass, true)
+        if err != nil {
+            sendError(ctx, errors.Errorf("References: %d: %s", refType, err), errChan, logger)
+            return err
+        }
+        refs = append(refs, classReferences...)
+    }
     logger.Debugf("found %d child refs\n", len(refs))
     for _, rn := range refs {
         wg.Add(1)
         go browse(ctx, rn, def.Path, level+1, logger, def.NodeID.String(), nodeChan, errChan, wg, browseHierarchicalReferences, opcuaBrowserChan)
     }
     return nil
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 52bdc8a and 3f6589c.

📒 Files selected for processing (1)
  • opcua_plugin/browse.go (2 hunks)

Copy link
Member

@JeremyTheocharis JeremyTheocharis left a comment

Choose a reason for hiding this comment

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

Can you add a test for this behaviour? have you checked with mateusz whether this is actually the case?

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.

Actionable comments posted: 3

🧹 Nitpick comments (2)
opcua_plugin/browse.go (2)

363-366: Consider adding context cancellation check before spawning goroutines.

While the goroutine management is generally correct, spawning new goroutines without checking the context could lead to unnecessary work if the context is already cancelled.

Add a context check:

-			wg.Add(1)
-			go browse(ctx, child, def.Path, level+1, logger, def.NodeID.String(), nodeChan, errChan, wg, browseHierarchicalReferences, opcuaBrowserChan)
+			select {
+			case <-ctx.Done():
+				return ctx.Err()
+			default:
+				wg.Add(1)
+				go browse(ctx, child, def.Path, level+1, logger, def.NodeID.String(), nodeChan, errChan, wg, browseHierarchicalReferences, opcuaBrowserChan)
+			}

98-101: Improve logging consistency and performance.

The logging patterns are inconsistent and could impact performance due to string concatenation. The [IMPORTANT] tag usage is also inconsistent.

Consider using structured logging and consistent formatting:

-	logger.Debugf("[IMPORTANT] browse function called with level: %d, path: %s, parentNodeId: %s", level, path, parentNodeId)
-	logger.Debugf("[IMPORTANT] The namespace for node %s is %s", n.ID().String(), namespace)
+	logger.Debugf("browse: start level=%d path=%s parentNodeId=%s", level, path, parentNodeId)
+	logger.Debugf("browse: namespace=%s nodeId=%s", namespace, n.ID().String())

-	logger.Debugf("NodeClass attr[0].Status for node %s is %s", n.ID().String(), attrs[0].Status)
-	logger.Debugf("NodeClass attr[0].Value for node %s is %s", n.ID().String(), attrs[0].Value.Int())
+	logger.Debugf("browse: attribute=NodeClass status=%s value=%d nodeId=%s", 
+		attrs[0].Status, attrs[0].Value.Int(), n.ID().String())

Also applies to: 148-149, 172-173, 220-221, 255-256

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3f6589c and 3a44b77.

📒 Files selected for processing (3)
  • opcua_plugin/browse.go (15 hunks)
  • opcua_plugin/opcua_node_wrapper.go (2 hunks)
  • opcua_plugin/opcua_unittest_test.go (3 hunks)
🧰 Additional context used
🪛 GitHub Actions: main
opcua_plugin/browse.go

[error] 314-314: Multiple goroutines crashed while browsing OPC UA nodes

🔇 Additional comments (2)
opcua_plugin/opcua_node_wrapper.go (2)

25-26: LGTM! Clean interface extension.

The addition of the Node() method to the NodeBrowser interface is clean and well-defined.


73-75: LGTM! Simple and correct implementation.

The implementation of Node() in OpcuaNodeWrapper is straightforward and correctly returns the underlying node.

@@ -489,6 +490,7 @@
attributes []*ua.DataValue
browseName *ua.QualifiedName
referenceNodes map[uint32][]NodeBrowser
node *opcua.Node
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Codebase verification

Add tests for the Node() method in MockOpcuaNodeWraper

The codebase analysis confirms that while the Node() method is implemented, it lacks test coverage. Other methods in MockOpcuaNodeWraper are properly tested, but there are no test cases verifying the behavior of the Node() method.

  • Add test cases in opcua_unittest_test.go to verify:
    1. Node() returns the correct node when set
    2. Node() returns nil when not set (current default behavior)
🔗 Analysis chain

Add tests for the new Node() method.

While the implementation is correct, there are no tests verifying the behavior of the new Node() method in the mock wrapper.

Consider adding test cases to verify:

  1. Node() returns the correct node when set
  2. Node() returns nil when not set

Also applies to: 573-575

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check if there are any tests for the Node() method
rg -A 5 "TestMockOpcuaNodeWraper.*Node" || echo "No tests found for Node() method"

Length of output: 116


Script:

#!/bin/bash
# Find MockOpcuaNodeWraper implementation
ast-grep --pattern 'type MockOpcuaNodeWraper struct {
  $$$
}

func ($_ *MockOpcuaNodeWraper) Node() $_ {
  $$$
}'

# Find any test usage of MockOpcuaNodeWraper
rg -A 5 "MockOpcuaNodeWraper" opcua_plugin/opcua_unittest_test.go

Length of output: 2571

Comment on lines +211 to 213
logger.Debugf("[IMPORTANT] The description is nil. So setting the node class to object for node %s", n.ID().String())
def.NodeClass = ua.NodeClassObject // by setting it as an object, we will not subscribe to it
// no need to return here, as we can continue without the Description
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Standardize error handling for attribute access failures.

The code inconsistently handles attribute access failures. In some cases, it sets NodeClass to Object as a fallback, which might hide actual issues. This pattern is repeated in multiple places with slightly different implementations.

Consider extracting this pattern into a helper function:

+func handleAttributeError(def *NodeDef, logger Logger, attributeName string) {
+	logger.Warnf("Attribute %s is not readable. Setting node class to object", attributeName)
+	def.NodeClass = ua.NodeClassObject
+}

-		logger.Debugf("[IMPORTANT] The description is nil. So setting the node class to object for node %s", n.ID().String())
-		def.NodeClass = ua.NodeClassObject // by setting it as an object, we will not subscribe to it
+		handleAttributeError(&def, logger, "Description")

-		logger.Debugf("[IMPORTANT] The data type is not readable for node %s. So setting the node class to object", n.ID().String())
-		def.NodeClass = ua.NodeClassObject // by setting it as an object, we will not subscribe to it
+		handleAttributeError(&def, logger, "DataType")

Also applies to: 305-307

Comment on lines +314 to +318
originalNodeClass, err := n.Node().NodeClass(ctx)
if err != nil {
logger.Debugf("Error getting the original node class for node %s: %s", n.ID().String(), err)
}
logger.Debugf("originalNodeClass for node %s is %s. def.NodeClass after the fallback condition is %s", n.ID().String(), originalNodeClass, def.NodeClass)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix potential nil pointer dereference causing goroutine crashes.

The direct call to n.Node().NodeClass() can cause a panic if Node() returns nil. This is causing goroutine crashes as reported in the pipeline failures.

Add a nil check before accessing the node:

-	originalNodeClass, err := n.Node().NodeClass(ctx)
-	if err != nil {
-		logger.Debugf("Error getting the original node class for node %s: %s", n.ID().String(), err)
-	}
-	logger.Debugf("originalNodeClass for node %s is %s. def.NodeClass after the fallback condition is %s", n.ID().String(), originalNodeClass, def.NodeClass)
+	if node := n.Node(); node != nil {
+		if originalNodeClass, err := node.NodeClass(ctx); err != nil {
+			logger.Debugf("Error getting the original node class for node %s: %s", n.ID().String(), err)
+		} else {
+			logger.Debugf("originalNodeClass for node %s is %s. def.NodeClass after the fallback condition is %s", n.ID().String(), originalNodeClass, def.NodeClass)
+		}
+	}
📝 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. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
originalNodeClass, err := n.Node().NodeClass(ctx)
if err != nil {
logger.Debugf("Error getting the original node class for node %s: %s", n.ID().String(), err)
}
logger.Debugf("originalNodeClass for node %s is %s. def.NodeClass after the fallback condition is %s", n.ID().String(), originalNodeClass, def.NodeClass)
if node := n.Node(); node != nil {
if originalNodeClass, err := node.NodeClass(ctx); err != nil {
logger.Debugf("Error getting the original node class for node %s: %s", n.ID().String(), err)
} else {
logger.Debugf("originalNodeClass for node %s is %s. def.NodeClass after the fallback condition is %s", n.ID().String(), originalNodeClass, def.NodeClass)
}
}
🧰 Tools
🪛 GitHub Actions: main

[error] 314-314: Multiple goroutines crashed while browsing OPC UA nodes

@kanapuli kanapuli closed this Jan 20, 2025
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