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

debug: Adding debugger #6877

Merged
merged 23 commits into from
Aug 28, 2024
Merged

Conversation

johanfylling
Copy link
Contributor

Fixes: #6876

(mostly renaming)

Signed-off-by: Johan Fylling <[email protected]>
Signed-off-by: Johan Fylling <[email protected]>
charlieegan3
charlieegan3 previously approved these changes Jul 24, 2024
Copy link
Contributor

@charlieegan3 charlieegan3 left a comment

Choose a reason for hiding this comment

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

This a tremendous effort Johan! I've tested this via the Regal PR and seen it work. Most of my comments are related to comments and documentation, please feel free to take or leave as you please.

Amazing work! 👏

Copy link
Member

@ashutosh-narkar ashutosh-narkar left a comment

Choose a reason for hiding this comment

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

This looks great. I had a high-level question about the direction of this change. From #6876 I read this as OPA providing a debugger interface for applications to leverage but it looks like we're adding a debugger implementation in OPA. Did I miss something about the intent behind this change?

@johanfylling
Copy link
Contributor Author

@ashutosh-narkar , perhaps the ticket is a bit sparse and poorly worded, but the intent has always been to create a debugger in OPA that 3rd parties can interface with through the SDK.

The point at which to contest this would of course be now, before this becomes a part of OPA proper.

I think it makes sense to have this be a tightly integrated component, as it'll make it less likely changes to OPA will break the debugger. And as history tells us, even though most of this functionality is possible as a stand-alone project, it's very unlikely that any 3rd party outside of the OPA team will develop and maintain this.

Signed-off-by: Johan Fylling <[email protected]>
@ashutosh-narkar
Copy link
Member

perhaps the ticket is a bit sparse and poorly worded, but the intent has always been to create a debugger in OPA that 3rd parties can interface with through the SDK.

Thanks for the clarification. I thought we are creating an interface for a debugger which 3rd parties can hook up their own implementations.

And as history tells us, even though most of this functionality is possible as a stand-alone project, it's very unlikely that any 3rd party outside of the OPA team will develop and maintain this.

That's probably true. Then the question is why can't this be a stand-alone project that the OPA team maintains, like the OPA-Envoy plugin for example. If the team is maintaining it, we'd anyways keep it updated with the latest OPA changes irrespective of where it lives. Also in terms of external contributors who want to contribute to tooling for example, this would be a good one to get started with. If we're getting some performance/usage/implementation benefits by keeping this in OPA then it makes sense to have it tightly coupled. Otherwise my suggestion would be to define the interface in OPA and have a mechanism to hook this in.

@johanfylling
Copy link
Contributor Author

Implementation-wise, the benefit is access to internal packages and easier/quicker turnover for fixes and new features that require changes to OPA, as we don't have a secondary project we need to either stagger releases for or keep up-to-date during release cycles. E.g. if we make a change in OPA that has adverse effects for the debugger, then we wouldn't know until the debugger tests are run on the new changes. This is of course possible to automate, but it's a bit cumbersome to maintain.

Usage-wise, 3rd parties that integrate with the debugger would have yet another dependency to keep track of (arguably a minor detail). Discoverability is another issue. If a user has issues with the debugger, they'll probably file a bug report with OPA first, which we'll need to refer over to the debugger project.

By themselves, these might not be super strong arguments against a separate project, but at the same time, I don't think it's a gross feature creep to have the project that's responsible for compiling and executing a language to also have the feature to debug it.

@anderseknert
Copy link
Member

Then the question is why can't this be a stand-alone project that the OPA team maintains, like the OPA-Envoy plugin for example. If the team is maintaining it, we'd anyways keep it updated with the latest OPA changes irrespective of where it lives.

That's still another thing we'd need to remember to do, and actually do, every time there's a new OPA release. Considering our limited resources, I think it makes sense to include this in OPA core for the sake of minimizing maintainer burden. I'm sure there are technical/performance benefits of that too, but others are better qualified to judge that.

Copy link
Member

@ashutosh-narkar ashutosh-narkar left a comment

Choose a reason for hiding this comment

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

This is great work! I haven't gone through the whole PR yet but will do that in this week. Adding some comments for the parts I've looked at so far.

Comment on lines +114 to +123
var newBps breakpointList
for _, bp := range bps {
if bp.ID() != id {
newBps = append(newBps, bp)
} else {
removed = bp
}
}
bc.breakpoints[path] = newBps
}
Copy link
Member

Choose a reason for hiding this comment

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

Did you consider using a linked list for storing the breakpoints. I don't think performance is priority here even if this becomes too large but probably would make the operation cleaner.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Are you suggesting to change the root container from a map to a linked list, or only the array value?
We're diving into this collection quite often to find breakpoints, so it makes sense to do a first lookup by file, even though the number of breakpoints are unlikely to ever get extremely high.

Copy link
Member

Choose a reason for hiding this comment

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

even though the number of breakpoints are unlikely to ever get extremely high.

Doesn't matter then. All good.

if i > 0 {
buf.WriteString(", ")
}
_, _ = fmt.Fprintf(buf, "%s:%d\n", path, bp.Location().Row)
Copy link
Member

Choose a reason for hiding this comment

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

Could we just use the stringer on the breakpoint or there is some reason behind not including the id?

ResumeAll() error

// StepOver executes the next expression in the current scope and then stops on the next expression in the same scope,
// not stopping on expressions un sub-scopes; e.g. execution of referenced rule, called function, comprehension, or every expression.
Copy link
Member

Choose a reason for hiding this comment

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

Typo: un

SetBreakpoints(locations []location.Location) ([]Breakpoint, error)

// AddBreakpoint sets a breakpoint at the given location.
AddBreakpoint(loc location.Location) (Breakpoint, error)
Copy link
Member

Choose a reason for hiding this comment

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

Nit: let's stick with AddBreakpoint(s) or SetBreakpoint(s)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I think we can drop SetBreakpoints 👍

// Terminate stops all threads in the session.
Terminate() error

// TODO: Add Stop(ThreadID) func for stopping (pausing) a thread's execution.
Copy link
Member

Choose a reason for hiding this comment

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

Should we remove this or are you planning to make it part of the current work? If not, we can remove and open an issue to track this.

return nil, fmt.Errorf("failed to prepare query for evaluation: %v", err)
}

if err := store.Commit(ctx, txn); err != nil {
Copy link
Member

Choose a reason for hiding this comment

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

What are we committing here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We're committing any data added during PrepareForEval. I found that if we don't do this, then we won't be able to read this data from the store in thread.dataVars(). This might not be the correct place or method to do this, though; so if you can point me to a more proper solution, I'm all ears 🙂.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for the context. Maybe add a comment mentioning the same and we can improve later if needed.

return nil
}

func (s *session) StepOver(threadID ThreadID) error {
Copy link
Member

Choose a reason for hiding this comment

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

Nit: StepOver, StepOut and StepIn have some duplicate code which we could move in a helper.

}

if s.skipOp(e.Op) {
// FIXME: Should we only skip an event as long as we're within the same query scope?
Copy link
Member

Choose a reason for hiding this comment

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

What's the context here? Would be good to fix this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was an old comment we can remove. I think we're still gonna need to skip "undesired" operations if they're the next op when entering or leaving a new query level/context.

return input, nil
}

type session struct {
Copy link
Member

Choose a reason for hiding this comment

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

Can this be accessed concurrently? I didn't see any locks so maybe not?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the primary entrypoint for e.g. implementations of the DAP protocol, so we can't rule out concurrent access completely. It should be benign to add a global mutex. I'll look into fixing that.

func (s *session) AddBreakpoint(loc location.Location) (Breakpoint, error) {
if s == nil {
return nil, fmt.Errorf("no active debug session")
}
Copy link
Member

Choose a reason for hiding this comment

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

Hmm interesting that SetBreakpoints clears existing breakpoints before adding new ones.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

"Set" implies replacing what already exists, whereas add does not.
This was however a misread of the DAP protocol from me, where I originally thought setting breakpoints was a global operation, but it's actually a per-file operation. I solved this by instead making the user of the SDK make that distinction by using a combination of Breakpoints(), AddBreakpoint(), and RemoveBreakpoint().

debug/thread.go Outdated
qid = e.QueryID
}

// FIXME: Compare against c.ParentID instead?
Copy link
Member

Choose a reason for hiding this comment

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

Aren't we using similar logic in stepOver? What's the concern here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, this isn't the only place where this could be something to consider. Likely, we wouldn't solve anything by doing this, though, as I think we could be "popped out" more than one query level at a time; making the parent ID irrelevant 🤔 .
I'll remove this comment.

"github.com/open-policy-agent/opa/topdown"
)

type stack interface {
Copy link
Member

Choose a reason for hiding this comment

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

Should this be called trace? Also we don't need to make this public?

Copy link
Contributor Author

@johanfylling johanfylling Aug 26, 2024

Choose a reason for hiding this comment

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

There are two sides to this coin: from the perspective of topdown, this is a tracer; but from the view of the debugger, it behaves as a stack, so we're letting the naming reflect that perspective.

Currently, I don't think this needs to be public, but that could change in the future.

@@ -19,7 +42,7 @@ type virtualCacheElem struct {
undefined bool
}

func newVirtualCache() *virtualCache {
func NewVirtualCache() VirtualCache {
Copy link
Member

Choose a reason for hiding this comment

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

This is being used by the debugger implementation hence we're making this public. Couldn't we use an internal package for this and avoid exposing this?

Copy link
Member

Choose a reason for hiding this comment

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

Nvm. I just saw we're doing func (q *Query) WithVirtualCache(vc VirtualCache) *Query

@@ -11,6 +11,7 @@ import (
"time"
)

// FIXME: Abort long running tests
Copy link
Member

Choose a reason for hiding this comment

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

Anything you plan to do in this PR about this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No. This was a thought that struck me when passing by. I'll create a ticket for tracking.

Copy link
Member

@ashutosh-narkar ashutosh-narkar left a comment

Choose a reason for hiding this comment

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

@johanfylling are you planning to add some documentation for this?

Copy link

netlify bot commented Aug 28, 2024

Deploy Preview for openpolicyagent ready!

Name Link
🔨 Latest commit 02cc5e9
🔍 Latest deploy log https://app.netlify.com/sites/openpolicyagent/deploys/66cf419b12ef410008968a12
😎 Deploy Preview https://deploy-preview-6877--openpolicyagent.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

}
}

variables, err := session.Variables(localScope.VariablesReference())
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a contender for some UX improvements.

Copy link
Member

@ashutosh-narkar ashutosh-narkar left a comment

Choose a reason for hiding this comment

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

This is an excellent addition 👏

@johanfylling johanfylling merged commit 3ac5104 into open-policy-agent:main Aug 28, 2024
28 checks 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.

Add Debugger interface to SDK
4 participants