-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
debug: Adding debugger #6877
Conversation
Fixes: open-policy-agent#6876 Signed-off-by: Johan Fylling <[email protected]>
(mostly renaming) Signed-off-by: Johan Fylling <[email protected]>
Signed-off-by: Johan Fylling <[email protected]>
Signed-off-by: Johan Fylling <[email protected]>
Signed-off-by: Johan Fylling <[email protected]>
There was a problem hiding this 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! 👏
Signed-off-by: Johan Fylling <[email protected]>
There was a problem hiding this 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?
@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]>
Thanks for the clarification. I thought we are creating an interface for a debugger which 3rd parties can hook up their own implementations.
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. |
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. |
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. |
Signed-off-by: Johan Fylling <[email protected]>
Signed-off-by: Johan Fylling <[email protected]>
Signed-off-by: Johan Fylling <[email protected]>
There was a problem hiding this 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.
var newBps breakpointList | ||
for _, bp := range bps { | ||
if bp.ID() != id { | ||
newBps = append(newBps, bp) | ||
} else { | ||
removed = bp | ||
} | ||
} | ||
bc.breakpoints[path] = newBps | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
debug/breakpoint.go
Outdated
if i > 0 { | ||
buf.WriteString(", ") | ||
} | ||
_, _ = fmt.Fprintf(buf, "%s:%d\n", path, bp.Location().Row) |
There was a problem hiding this comment.
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
?
debug/debugger.go
Outdated
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. |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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
👍
debug/debugger.go
Outdated
// Terminate stops all threads in the session. | ||
Terminate() error | ||
|
||
// TODO: Add Stop(ThreadID) func for stopping (pausing) a thread's execution. |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 🙂.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
debug/debugger.go
Outdated
} | ||
|
||
if s.skipOp(e.Op) { | ||
// FIXME: Should we only skip an event as long as we're within the same query scope? |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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") | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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()
.
Signed-off-by: Johan Fylling <[email protected]>
Signed-off-by: Johan Fylling <[email protected]>
…key list Signed-off-by: Johan Fylling <[email protected]>
debug/thread.go
Outdated
qid = e.QueryID | ||
} | ||
|
||
// FIXME: Compare against c.ParentID instead? |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this 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?
* Removing old FIXME Signed-off-by: Johan Fylling <[email protected]>
Signed-off-by: Johan Fylling <[email protected]>
✅ Deploy Preview for openpolicyagent ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
} | ||
} | ||
|
||
variables, err := session.Variables(localScope.VariablesReference()) |
There was a problem hiding this comment.
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.
Signed-off-by: Johan Fylling <[email protected]>
There was a problem hiding this 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 👏
Fixes: #6876