Multi-site aware PSGI application development
Asked Answered
J

2

9

The Plack::Builder allows mount multiple hosts, e.g. something as the following snippet:

my @sites = load_site_names();
my $apps;
for my $site (@sites) {
    $apps->{$site} = Some::PsgiFramework::MyApp->new( config => get_config($site) );
}

use Plack::Builder;
builder {
    for my $site (@sites) {
        mount "$site" => $apps->{$site};
    }
    mount '/' => sub { ... };
}

e.g.

  • the load_site_names returns a list of sites like http://example.com , http://some.other.site.com, ...
  • every "virtual-host" will use the same Some::PsgiFramework::MyApp
  • just their config is different

I need exactly the above - need develop one simple web-app which should be deployed for hunderts of different (low-traffic) sites and don't want setup an different PSGI server for each site.

However, the author of the Plack itself says (in the Plack::Request)

Note that this module is intended to be used by Plack middleware developers and web application framework developers rather than application developers (end users).

Writing your web application directly using Plack::Request is certainly possible but not recommended: it's like doing so with mod_perl's Apache::Request: yet too low level.

If you're writing a web application, not a framework, then you're encouraged to use one of the web application frameworks that support PSGI (http://plackperl.org/#frameworks), or see modules like HTTP::Engine to provide higher level Request and Response API on top of PSGI.

And this is the problem.

I checked many of different PSGI based frameworks in the MetaCPAN. And AFAIK each is singleton based, e.g. doesn't allows write applications which could be shared (mounted) many times for different sites in the same app.psgi.

So the questions are:

  • missed I something in the MetaCPAN (or in the docs), and here exists any (lighweight) web-framework which allows develop applications mountable many times in the app.psgi?
  • or i'm forced to develop Just Another My Own PSGI Framework? (To be honest, I not checked the catalyst - as it is too heavy-weight)
  • or just badly understand the "mounting"?
Justen answered 26/3, 2016 at 1:21 Comment(7)
So is the code base the same for each site you will serve probably with different data? Possible you could add a en.wikipedia.org/wiki/Reverse_proxy in front of your server to serve all the addressesPilatus
You probably don't want to use Plack::Builder for mounting hundreds of different PSGI apps. According to the Plack::App::URLMap docs: "If you map (or mount with Plack::Builder) N applications, Plack::App::URLMap will need to at most iterate through N paths to match incoming requests. It is a good idea to use map only for a known, limited amount of applications, since mounting hundreds of applications could affect runtime request performance."Musk
Having said that, you can select which config file to use in a Dancer2 app via an environment variable, and you can mount the same Dancer2 app multiple times, although I haven't figured out how to do both of these at once. Maybe try asking if it's possible on the dancer-users mailing list.Musk
What about using a single instance of Some::PsgiFramework::MyApp and using $env->{HTTP_HOST} (or the framework's equivalent) to customize each site? That's what I do for some multilingual sites.Subversion
@Subversion So, is short: what i should use in place of the Some::PsgiFramework:? Which one suports the multi-site mounting on the framework level (build in support - not my own "hacking")?Justen
Every PSGI app can be mounted multiple times, framework or not. It seems that your actual question is "Which PSGI frameworks support reading different config files depending on the HTTP host?"Subversion
@Subversion No, the question is as above. Youre talking about some "reading config values runtime" - which is an different thing. I want create many instances of the same app. (Please read the code above). When the app is instantiated it knows nothing about the $env->{HTTP_HOST}. (because the request handling doesn't even started).Justen
H
1

Building the dispatcher in Plack

There is an alternative to Plack::App::URLMap called Plack::App::HostMap that does the lookups way faster because it uses a hash internally, not an array. So there is no iterating going on. It just does a hash lookup, and those are really fast in Perl.

The trade-off is that now you can only use constant host names. So if your list is something like this:

example.org
example.com
example.de
example.am
example.cx

Or with sub-domains like:

one.example.org
two.example.org
three.example.org
four.example.org
five.example.org
six.example.org

Then this is perfect. On the other hand I am not sure if it supports URLs that also have a constant path part, like http://foo.example.org/bar, where there are lots of foos, but all of them share the same /bar path where the app is mounted. The module does not have any tests at all, and I couldn't try it. If you look at the changes, there has at least been one person suggesting additional features, so someone other than the author is using it.

To use it, you would switch from Plack::Builder to using the the Plack::App::HostMap as an app that you call methods on.

use Plack::App::HostMap;

# set up %apps (e.g. foo.example.org, bar.example.org)

my $host_map = Plack::App::HostMap->new;

for my $site (@sites) {
    $host_map->map( $site => $apps->{$site} );
}

You're not telling us what the / route should do, but essentially it also needs a host. If your server has a lot of hostnames then all of them will respond to this request. That's the whole idea of what you want to do. But what hostname is for /? So the best thing to do would be to include an additional line for the sub { ... } slash-app with the real hostname. Maybe that's a control panel or something. So hook it up to the actual URL.

 $host_map->map( "example.org" => sub { ... } );

A web framework to do this with

The singleton is not really the problem here. It seems not possible to get Dancer2 to load different configs or environments with the same one. I have not tried Mojo, Web::Simple or Catalyst for this use case.

I did try a lot with D2, and the closest I got was having a / route in MyApp, and this PSGI app. Note this does not work.

use Plack::Builder;

my $builder = Plack::Builder->new;
foreach my $name (qw/development production/) {
    $builder->mount(
        "/$name" => builder {
            eval <<"APP";
package MyApp::$name {
    use Dancer2;
    use MyApp with => { environment => "$name" };
}
APP

            "MyApp::$name"->to_app;
        }
    );
}

$builder->to_app;

It uses the default skeleton generated with dancer2 -a MyApp and unchanged environment files. The dispatching from Plack works, but Dancer2 gets confused.

HTTP::Server::PSGI: Accepting connections at http://0:5000/
[MyApp::production:4896] core @2017-02-10 02:14:42> looking for get / in /home/julien/perl5/perlbrew/perls/perl-5.20.1/lib/site_perl/5.20.1/Dancer2/Core/App.pm l. 35
[MyApp::production:4896] core @2017-02-10 02:14:42> Entering hook core.error.init in (eval 49) l. 1
[MyApp::production:4896] core @2017-02-10 02:14:42> Entering hook core.error.before in (eval 49) l. 1
[MyApp::production:4896] core @2017-02-10 02:14:42> Entering hook core.error.after in (eval 49) l. 1
127.0.0.1 - - [10/Feb/2017:02:14:42 +0100] "GET /production/ HTTP/1.1" 404 456 "-" "curl/7.47.0"
[MyApp::development:4896] core @2017-02-10 02:18:06> looking for get  in /home/julien/perl5/perlbrew/perls/perl-5.20.1/lib/site_perl/5.20.1/Dancer2/Core/App.pm l. 35
[MyApp::development:4896] core @2017-02-10 02:18:06> Entering hook core.error.init in (eval 49) l. 1
[MyApp::development:4896] core @2017-02-10 02:18:06> Entering hook core.error.before in (eval 49) l. 1
[MyApp::development:4896] core @2017-02-10 02:18:06> Entering hook core.error.after in (eval 49) l. 1
127.0.0.1 - - [10/Feb/2017:02:18:06 +0100] "GET /development HTTP/1.1" 404 457 "-" "curl/7.47.0"

The idea was to use the same package file and subclass it to get the different config in via with.

However, it is possible to just define the same app in the loop, over and over again. You could probably move the route handler out use a code ref like get '/' => \&main::get_slash, where sub get_slash is not in the eval.

use Plack::Builder;

my $builder = Plack::Builder->new;
foreach my $name (qw/development production/) {
    $builder->mount(
        "/$name" => builder {
            eval <<"APP";
package MyApp::$name {
use Dancer2;
    use Data::Printer;

    set environment => "$name";

    get "/" => sub { np(config) }
}
APP

            "MyApp::$name"->to_app;
        }
    );
}

$builder->to_app;

The string eval is not as evil as it looks here as that code only gets run at startup. D2 will internally keep track of all the apps that you created programmatically here. But I have no idea how performant that is.

Hypertrophy answered 10/2, 2017 at 1:23 Comment(0)
K
1

I believe that quoted documentation is intended more for Plack::Request and not Plack::Builder.

It is perfectly acceptable to mount various applications (e.g. Dancer/Catalyst/Mojolicious/homegrown app) using Plack::Builder and indeed this is quite common.

Khabarovsk answered 17/2, 2017 at 13:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.