croservices / cro

Development tools for building services and distributed systems in Raku using the Cro libraries.
https://cro.services/
Artistic License 2.0
88 stars 34 forks source link

Dynamic variables inaccessible inside get, post, etc routines #46

Open scmorrison opened 6 years ago

scmorrison commented 6 years ago

Dynamic variables get lost inside of route HTTP method (get, post, etc.) routines:

use Cro::HTTP::Router;
use Cro::HTTP::Server;              

my $*dyn = 'I am dynamic';               

my $application = route {        

    say $*dyn; # works

    {
      say $*dyn; # works
    }

    say -> {
        $*dyn; # works
    }();

    get -> {
        content 'text/html', "{$*dyn}"; # Dynamic variable $*dyn not found
    }
}

my Cro::Service $hello-service = Cro::HTTP::Server.new(
    :host('localhost'), :port(8888), :$application
);
$hello-service.start;
react whenever signal(SIGINT) {
    $hello-service.stop;
    exit;
}
jnthn commented 6 years ago

I'd not expect this to work. Dynamic variables must be in dynamic scope to be visible. The route block is run right at the point it is encountered, in order to build the router transform that's stored in $application. Thus it is out of dynamic scope by the time any of the handlers are run. Further to that, Cro runs handlers on the thread pool (not directly, but as a result of using Supply-based concurrency), and each thread is its own dynamic scope.

I'm guessing this is golfed from a more practical problem; what were you originally trying to achieve?

CurtTilmes commented 6 years ago

I'm not the OP, but I've tried to do stuff like this.

In the past, I've used Mojolicious which has concepts like the stash and helpers that are good for maintaining not quite global variables that are visible throughout the application and routes.

What is the best way to handle those sort of variables?

scmorrison commented 6 years ago

@jnthn, thanks for the explanation. This was just something I stumbled on. I was experimenting with dynamic variables and trying to see if they were accessible across nested modules when importing from separate .pm6 files. I threw a route {} block in one of the nested modules and lost access to the dynamic variable.

To illustrate your point about how the dynamic variable is not carried across threads / each thread has its own dynamic scope:

my $*dyn = 'I am dynamic';  

Thread.start({
    try {
        say $*dyn; # Dynamic variable $*dyn not found
        CATCH { default { say .Str } }
    }
});
jnthn commented 6 years ago

@CurtTilmes State is something we have to be really quite careful about, given that requests may be processed concurrently in the thread pool - and with HTTP/2.0 that applies even to requests on a single connection. That said, I don't think the state being dealt with by the Mojo stash really is "global-ish" in nature, but really scoped to the lifetime of a particular request - and we can rely on that being manipulated by one thing at a time.

I guess the most immediate application for attaching Extra Stuff to requests is middleware being able to pass extra data onward. That can already be done relatively easily using a Perl 6 built-in feature: mixins.

role RequestDataObject {
    has $.data-object;
}
...
$request does RequestDataObject($the-data)

Which is not quite as lightweight as having a hash to poke things into, but has its attractions: the lifetime is clearly that of the request, one can smartmatch to see if the extra state is there, typo'ing the property name gives a missing method error with typo suggestions, more refactoring potential, etc.

About helpers (I think that's my least favorite name in programming for anything... :-)), that looks like a way to be able to write things that one can call in request/response processing logic and have access to the request or response. If that's all they are, then I suggest:

  1. Write a module that does use Cro::HTTP::Router and then write the "helpers" in subs that use request and/or response (these are resolved using dynamic variables, so it doesn't matter the code using them isn't in the lexical scope of the request handler itself). In fact, one can use things like header, not-found and all the rest in such "helpers" too, since they work in terms of dynamic request and response.
  2. Mark the subs is export
  3. Just use them (can even take advantage of lexical import to just use them inside a particular route block).

I don't think there's any need for an object-y approach here; let functions be functions.

In general, I'm open to adding things to Cro as needed, but if we can discover elegant solutions that take advantage of Perl 6's numerous built-in features without feeling boilerplate-y, then to me that's preferable.