Skip to content

Conversation

@MusicDin
Copy link
Member

@MusicDin MusicDin commented May 7, 2025

This PR aims to reduce the usage of http.Request outside the API handlers. The main requirement for this is to stop passing an actual request around, and pass only the request context instead.

The second change addresses the types returned from the BL functions. Instead of returning the responses, we want to get concrete types and a potential error. This also affected forwarding response, which is forwards the request when the response is rendered. Now, we return a ForwardRequiredError instead, which is handled by response.SmartError.

Only a subset of handlers are addressed, and the remaining will be done once we agree upon this approach.

@github-actions github-actions bot added the API Changes to the REST API label May 7, 2025
@MusicDin MusicDin force-pushed the feat/refactor-handlers branch 2 times, most recently from f5d93ea to 4196fa2 Compare May 8, 2025 09:32
@github-actions github-actions bot added the Documentation Documentation needs updating label May 8, 2025
@MusicDin MusicDin force-pushed the feat/refactor-handlers branch from 4196fa2 to 5cb758a Compare May 8, 2025 09:33
@github-actions github-actions bot removed the Documentation Documentation needs updating label May 8, 2025
@markylaing
Copy link
Contributor

I've had a quick look over this out of curiosity and I have a few initial thoughts:

  1. Since this is such a large scale refactor. I think it's worth standardising how requests are parsed. I have a few ideas for this.
  2. I think it's worth formalising what a request context is and what it should contain. Currently we have a lot of request.CtxKey definitions, where the true nature of the request typically needs to be computed from a combination of them (e.g. CtxUsername vs. CtxForwardedUsername have a very important distinction when it comes to auth). This could be something like:
type RequestContext interface {
    context.Context
    ID() string // ID of request
    IsAuthenticated() bool
    AuthenticationMethod() string // Always returning the authentication method of the original requestor, even if forwarded
    Identifier() string // Always returning the identifier of the original requestor, even if forwarded
    IsForwarded() bool // Indicate that the request has been forwarded.
    ForwardedFrom() string
    /* etc. */
}

Then, when a request arrives, an implementation of this interface is instantiated and passed around as a context.Context. Callers can then check if it's an internal call via type assertion (e.g. requestContext, ok := ctx.(RequestContext), if !ok { /* internal */).
4. It would be nice to have an interface for the business logic that looks similar to the client interface. This would separate handling of e.g. GetInstances and GetInstancesFull, meaning that implementations would be lots of smaller functions, rather than one giant handler that catches lots of cases.

@MusicDin
Copy link
Member Author

MusicDin commented May 8, 2025

Thanks for taking a look at this 🚀

We previously briefly spoken about that and I completely agree.
Although, I would personally avoid passing RequestContext as context.Context when this is not entirely necessary. This way we avoid the need for type assertion, and we can pass nil to emphasize that there is no request context (and make the linter happy at the same time).

Also we avoid confusion where functions accept multiple contexts someFunc(ctx context.Context, reqContext context.Context, ...).

And this still leaves us the room to pass the request context as context.Context if really necessary (e.g. in helper function). In such case, the receiver can still use the type assertion as per your suggestion instead of detecting the request context by checking if a specific context key is set.

  1. It would be nice to have an interface for the business logic that looks similar to the client interface. This would separate handling of e.g. GetInstances and GetInstancesFull, meaning that implementations would be lots of smaller functions, rather than one giant handler that catches lots of cases.

Could you elaborate on this one a bit? I'm a bit confused because those smaller functions generally accept different input arguments, and we would introduce an interface that just maps our current implementation?

@tomponline
Copy link
Member

I'm not entirely convinced on the need for an interface (as opposed to a concrete struct), is there going to be multiple implementations? If not then a concrete struct should be sufficient to hold the functions/variables, rather than needing to mask it behind an interface. See https://medium.com/@rahulgorai/why-interfaces-can-be-expensive-in-go-and-when-you-should-still-use-them-a1510c162851

@markylaing
Copy link
Contributor

Could you elaborate on this one a bit? I'm a bit confused because those smaller functions generally accept different input arguments, and we would introduce an interface that just maps our current implementation?

Currently we have handlers that accept various parameters and return different things based on path arguments and query parameters.

My meaning here is the business logic should never return e.g. []any to represent either []api.Instance or []api.InstanceFull based an a recursion parameter. Instead, the business logic should have something like:

GetInstanceNames(ctx context.Context, projectName string) ([]string, error)
GetInstances(ctx context.Context, projectName string) ([]api.Instance, error)
GetInstancesFull(ctx context.Context, projectName string) ([]api.InstanceFull, error)
GetInstanceNamesAllProjects(ctx context.Context) ([]string, error)
GetInstancesAllProjects(ctx context.Context) ([]api.Instance, error)
GetInstanceFullAllProjects(ctx context.Context) ([]api.InstanceFull, error)

Depending on the initial request parsing, the handler will call out to one of these functions. These functions would be almost identical to those defined in client/interfaces.go but with have a context parameter.

I'm not entirely convinced on the need for an interface (as opposed to a concrete struct), is there going to be multiple implementations? If not then a concrete struct should be sufficient to hold the functions/variables, rather than needing to mask it behind an interface. See https://medium.com/@rahulgorai/why-interfaces-can-be-expensive-in-go-and-when-you-should-still-use-them-a1510c162851

I agree, there's no reason for the interface. Instead we could just have a struct for this that is set in the request context. If this struct is not present then it is an internal request. This would just bundle the various existing context keys together to make them easier to use.

@MusicDin
Copy link
Member Author

MusicDin commented May 8, 2025

My meaning here is the business logic should never return e.g. []any to represent either []api.Instance or []api.InstanceFull based an a recursion parameter. Instead, the business logic should have something like: ...

Yep, that's the goal. Effectively replacing response.Response outputs with concrete types. I started with storage pools/volumes/snapshots, but will apply to all handlers once we are satisfied with the approach.

EDIT: Although the function names can be adjusted according to your suggestion. Currently I went with StorageVolumePostHandler for handlers and StorageVolumePost for BL. IMO, would be nicer to have StorageVolumePostHandler/StorageVolumePost for handlers and GetStorageVolume, GetStorageVolumeNames,... for BL.

@MusicDin MusicDin force-pushed the feat/refactor-handlers branch 2 times, most recently from a619cc3 to 6869fd5 Compare May 12, 2025 12:02
@github-actions github-actions bot removed the API Changes to the REST API label May 12, 2025
@MusicDin MusicDin changed the title lxd: Separate API handlers from business logic lxd: Pass around the request context instead of the request May 12, 2025
@MusicDin MusicDin force-pushed the feat/refactor-handlers branch from 6869fd5 to aebb719 Compare May 12, 2025 12:09
@MusicDin MusicDin force-pushed the feat/refactor-handlers branch 3 times, most recently from 0e8c145 to 224e2da Compare May 21, 2025 13:58
@MusicDin MusicDin marked this pull request as ready for review May 22, 2025 08:25
@MusicDin
Copy link
Member Author

@markylaing @tomponline Removed handler refactoring logic and just left the bits that pass around the request context instead of the http.Request. The request info is coming in a followup PR.

MusicDin added 23 commits June 9, 2025 08:19
@MusicDin MusicDin force-pushed the feat/refactor-handlers branch from a237414 to 3ebb69a Compare June 9, 2025 08:22
@MusicDin MusicDin force-pushed the feat/refactor-handlers branch from 3ebb69a to 2440371 Compare June 9, 2025 08:46
Copy link
Member

@tomponline tomponline left a comment

Choose a reason for hiding this comment

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

LGTM thanks!

@tomponline tomponline merged commit 8fe4e60 into canonical:main Jun 9, 2025
30 checks passed
@MusicDin MusicDin deleted the feat/refactor-handlers branch June 9, 2025 11:31
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.

3 participants