diff --git a/.idea/runConfigurations/Load_Balanced_Server__PRODUCTION_.xml b/.idea/runConfigurations/Load_Balanced_Server__PRODUCTION_.xml new file mode 100644 index 00000000..d6a3e0f0 --- /dev/null +++ b/.idea/runConfigurations/Load_Balanced_Server__PRODUCTION_.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/Multithreaded_Server__PRODUCTION_.xml b/.idea/runConfigurations/Multithreaded_Server__PRODUCTION_.xml new file mode 100644 index 00000000..a70ff592 --- /dev/null +++ b/.idea/runConfigurations/Multithreaded_Server__PRODUCTION_.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/bin/cluster.dart b/bin/cluster.dart index d9e5e936..14090b89 100644 --- a/bin/cluster.dart +++ b/bin/cluster.dart @@ -7,6 +7,5 @@ import 'common.dart'; import 'dart:isolate'; main(args, SendPort sendPort) async { - runZoned(startServer(args, clustered: true, sendPort: sendPort), - onError: onError); + runZoned(startServer(args, sendPort: sendPort), onError: onError); } diff --git a/bin/common.dart b/bin/common.dart index c4dba888..87f1135e 100644 --- a/bin/common.dart +++ b/bin/common.dart @@ -5,7 +5,11 @@ import 'package:angel_diagnostics/angel_diagnostics.dart'; import 'package:angel_hot/angel_hot.dart'; import 'package:intl/intl.dart'; -startServer(args, {bool clustered: false, SendPort sendPort}) { +/// Start a single instance of this application. +/// +/// If a [sendPort] is provided, then the URL of the mounted server will be sent through the port. +/// Use this if you are starting multiple instances of your server. +startServer(args, {SendPort sendPort}) { return () async { var app = await createServer(); var dateFormat = new DateFormat("y-MM-dd"); @@ -13,26 +17,36 @@ startServer(args, {bool clustered: false, SendPort sendPort}) { InternetAddress host; int port; - if (!clustered) { - host = new InternetAddress(app.properties['host']); - port = app.properties['port']; - } else { - host = InternetAddress.LOOPBACK_IP_V4; - port = 0; - } + // Load the right host and port from application config. + host = new InternetAddress(app.properties['host']); + // Listen on port 0 if we are using the load balancer. + port = sendPort != null ? 0 : app.properties['port']; + + // Log requests and errors to a log file. await app.configure(logRequests(logFile)); HttpServer server; // Use `package:angel_hot` in any case, EXCEPT if starting in production mode. + // + // With hot-reloading, our server will automatically reload in-place on file changes, + // for a faster development cycle. :) if (Platform.environment['ANGEL_ENV'] == 'production') server = await app.startServer(host, port); else { var hot = new HotReloader(() async { + // If we are hot-reloading, we need to provide a callback + // to use to start a fresh instance on-the-fly. var app = await createServer(); await app.configure(logRequests(logFile)); return app; - }, [new Directory('config'), new Directory('lib')]); + }, + // Paths we might want to listen for changes on... + [ + new Directory('config'), + new Directory('lib'), + new Directory('views') + ]); server = await hot.startServer(host, port); } diff --git a/bin/multi_server.dart b/bin/multi_server.dart index 65deaebc..9ccc5def 100644 --- a/bin/multi_server.dart +++ b/bin/multi_server.dart @@ -2,7 +2,9 @@ /// This is intended to replace Nginx in your web stack. /// Either use this or another reverse proxy, but there is no real /// reason to use them together. -library angel.multiserver; +/// +/// In most cases, you should run `scaled_server.dart` instead of this file. +library angel.multi_server; import 'dart:io'; import 'package:angel_compress/angel_compress.dart'; @@ -10,6 +12,10 @@ import 'package:angel_multiserver/angel_multiserver.dart'; final Uri cluster = Platform.script.resolve('cluster.dart'); +/// The number of isolates to spawn. You might consider starting one instance +/// per processor core on your machine. +final int nInstances = Platform.numberOfProcessors; + main() async { var app = new LoadBalancer(); // Or, for SSL: @@ -21,8 +27,8 @@ main() async { // Cache static assets - just to lower response time await app.configure(cacheResponses(filters: [new RegExp(r'images/\.*')])); - // Start up 5 instances of our main application - await app.spawnIsolates(cluster, count: 5); + // Start up multiple instances of our main application. + await app.spawnIsolates(cluster, count: nInstances); app.onCrash.listen((_) async { // Boot up a new instance on crash @@ -33,4 +39,5 @@ main() async { var port = 3000; var server = await app.startServer(host, port); print('Listening at http://${server.address.address}:${server.port}'); + print('Load-balancing $nInstances instance(s)'); } diff --git a/bin/scaled_server.dart b/bin/scaled_server.dart new file mode 100644 index 00000000..b42c8408 --- /dev/null +++ b/bin/scaled_server.dart @@ -0,0 +1,67 @@ +#!/usr/bin/env dart + +/// Most Angel applications will not need to use the load balancer. +/// Instead, you can start up a multi-threaded cluster. +library angel.scaled_server; + +import 'dart:io'; +import 'dart:isolate'; +import 'package:angel_compress/angel_compress.dart'; +import 'package:angel_multiserver/angel_multiserver.dart'; +import 'package:angel/angel.dart'; + +/// The number of isolates to spawn. You might consider starting one instance +/// per processor core on your machine. +final int nInstances = Platform.numberOfProcessors; + +main() { + var startupPort = new ReceivePort(); + List startupMessages = []; + + // Start up multiple instances of our application. + for (int i = 0; i < nInstances; i++) { + Isolate.spawn(isolateMain, [i, startupPort.sendPort]); + } + + int nStarted = 0; + + // Listen for notifications of application startup... + startupPort.listen((String startupMessage) { + startupMessages.add(startupMessage); + + if (++nStarted == nInstances) { + // Keep track of how many instances successfully started up, + // and print a success message when they all boot. + startupMessages.forEach(print); + print('Spawned $nInstances instance(s) of Angel.'); + } + }); +} + +void isolateMain(List args) { + int instanceId = args[0]; + SendPort startupPort = args[1]; + + createServer().then((app) async { + // Response compression via GZIP. + // + // See the documentation here: + // https://github.com/angel-dart/compress + app.responseFinalizers.add(gzip()); + + // Cache static assets - just to lower response time. + // + // See the documentation here: + // https://github.com/angel-dart/multiserver + // + // Here is an example of response caching: + // https://github.com/angel-dart/multiserver/blob/master/example/cache.dart + await app.configure(cacheResponses(filters: [new RegExp(r'images/\.*')])); + + var server = await app.startServer( + InternetAddress.ANY_IP_V4, app.properties['port'] ?? 3000); + + // Send a notification back to the main isolate + startupPort.send('Instance #$instanceId listening at http://${server.address.address}:${server.port}'); + }); +} diff --git a/lib/angel.dart b/lib/angel.dart index 2a8a5b2f..28668abc 100644 --- a/lib/angel.dart +++ b/lib/angel.dart @@ -9,8 +9,13 @@ import 'src/services/services.dart' as services; /// Creates and configures the server instance. Future createServer() async { - Angel app = new Angel(); + /// Passing `startShared` to the constructor allows us to start multiple + /// instances of our application concurrently, listening on a single port. + /// + /// This effectively lets us multi-thread the application. + var app = new Angel.custom(startShared); + /// Set up our application, using three plug-ins defined with this project. await app.configure(configuration.configureServer); await app.configure(services.configureServer); await app.configure(routes.configureServer); diff --git a/lib/src/config/config.dart b/lib/src/config/config.dart index e2b4e293..810f40db 100644 --- a/lib/src/config/config.dart +++ b/lib/src/config/config.dart @@ -3,26 +3,27 @@ library angel.config; import 'dart:io'; import 'package:angel_common/angel_common.dart'; -// import 'package:angel_multiserver/angel_multiserver.dart'; -import 'package:mongo_dart/mongo_dart.dart'; import 'plugins/plugins.dart' as plugins; /// This is a perfect place to include configuration and load plug-ins. configureServer(Angel app) async { - await app.configure(loadConfigurationFile()); - var db = new Db(app.mongo_db); - await db.open(); - app.container.singleton(db); - - await app.configure(mustache(new Directory('views'))); - await plugins.configureServer(app); - - - // Uncomment this to enable session synchronization across instances. - // This will add the overhead of querying a database at the beginning - // and end of every request. Thus, it should only be activated if necessary. + // Load configuration from the `config/` directory. // - // For applications of scale, it is better to steer clear of session use - // entirely. - // await app.configure(new MongoSessionSynchronizer(db.collection('sessions'))); + // See: https://github.com/angel-dart/configuration + await app.configure(loadConfigurationFile()); + + // Configure our application to render Mustache templates from the `views/` directory. + // + // See: https://github.com/angel-dart/mustache + await app.configure(mustache(new Directory('views'))); + + // Apply another plug-ins, i.e. ones that *you* have written. + // + // Typically, the plugins in `lib/src/config/plugins/plugins.dart` are plug-ins + // that add functionality specific to your application. + // + // If you write a plug-in that you plan to use again, or are + // using one created by the community, include it in + // `lib/src/config/config.dart`. + await plugins.configureServer(app); } diff --git a/lib/src/models/user.dart b/lib/src/models/user.dart index bf97c1df..52f4e11d 100644 --- a/lib/src/models/user.dart +++ b/lib/src/models/user.dart @@ -2,6 +2,26 @@ library angel.models.user; import 'package:angel_framework/common.dart'; +// Model classes in Angel, as a convention, should extend the `Model` +// class. +// +// Angel doesn't box you into using a specific ORM. In fact, you might not +// need one at all. +// +// The out-of-the-box configuration for Angel is to assume +// all data you handle is a Map. You might consider leaving it this way, and just +// (de)serializing data when you need typing support. +// +// If you use a `TypedService`, then Angel will perform (de)serialization for you automatically: +// https://github.com/angel-dart/angel/wiki/TypedService +// +// You also have the option of using a source-generated serialization library. +// Consider `package:owl` (not affiliated with Angel): +// https://github.com/agilord/owl +// +// The `Model` class has no server-side dependency, and thus you can use it as-is, cross-platform. +// This is good for full-stack applications, as you do not have to maintain duplicate class files. ;) + class User extends Model { @override String id; diff --git a/lib/src/routes/routes.dart b/lib/src/routes/routes.dart index 251b5ed5..3a80b033 100644 --- a/lib/src/routes/routes.dart +++ b/lib/src/routes/routes.dart @@ -1,43 +1,92 @@ /// This app's route configuration. library angel.routes; +import 'dart:io'; import 'package:angel_common/angel_common.dart'; import 'controllers/controllers.dart' as controllers; +/// Adds global middleware to the application. +/// +/// Use these to apply functionality to requests before business logic is run. +/// +/// More on the request lifecycle: +/// https://github.com/angel-dart/angel/wiki/Request-Lifecycle configureBefore(Angel app) async { app.before.add(cors()); } /// Put your app routes here! +/// +/// See the wiki for information about routing, requests, and responses: +/// * https://github.com/angel-dart/angel/wiki/Basic-Routing +/// * https://github.com/angel-dart/angel/wiki/Requests-&-Responses configureRoutes(Angel app) async { + // Render `views/hello.mustache` when a user visits the application root. app.get('/', (req, ResponseContext res) => res.render('hello')); } +/// Configures fallback middleware. +/// +/// Use these to run generic actions on requests that were not terminated by +/// earlier request handlers. +/// +/// Note that these middleware might not always run. +/// +/// More on the request lifecycle: https://github.com/angel-dart/angel/wiki/Request-Lifecycle configureAfter(Angel app) async { - // Uncomment this to proxy over pub serve while in development: - // await app.configure(new PubServeLayer()); - - // Static server at /web or /build/web, depending on if in production + // Uncomment this to proxy over `pub serve` while in development. + // This is a useful feature for full-stack applications, especially if you + // are using Angular2. // - // In production, `Cache-Control` headers will also be enabled. + // For documentation, see `package:angel_proxy`: + // https://github.com/angel-dart/proxy + // + // await app.configure(new PubServeLayer()); + + // Mount static server at /web or /build/web, depending on if + // you are running in production mode. `Cache-Control` headers will also be enabled. + // + // Read the following two sources for documentation: + // * https://medium.com/the-angel-framework/serving-static-files-with-the-angel-framework-2ddc7a2b84ae + // * https://github.com/angel-dart/static await app.configure(new CachingVirtualDirectory()); // Set our application up to handle different errors. + // + // Read the following two sources for documentation: + // * https://github.com/angel-dart/angel/wiki/Error-Handling + // * https://github.com/angel-dart/errors var errors = new ErrorHandler(handlers: { + // Display a 404 page if no resource is found. 404: (req, res) async => res.render('error', {'message': 'No file exists at ${req.path}.'}), + + // On generic errors, give information about why the application failed. + // + // An `AngelHttpException` instance will be present in `req.properties` + // as `error`. 500: (req, res) async => res.render('error', {'message': req.error.message}) }); + // Use a fatal error handler to attempt to resolve any issues that + // result in Angel not being able to send the user a response. errors.fatalErrorHandler = (AngelFatalError e) async { - var req = await RequestContext.from(e.request, app); - var res = new ResponseContext(e.request.response, app); - res.render('error', {'message': 'Internal Server Error: ${e.error}'}); - await app.sendResponse(e.request, req, res); + try { + // Manually create a request and response context. + var req = await RequestContext.from(e.request, app); + var res = new ResponseContext(e.request.response, app); + + // *Attempt* to render an error page. + res.render('error', {'message': 'Internal Server Error: ${e.error}'}); + await app.sendResponse(e.request, req, res); + } catch (_) { + // If execution fails here, there is nothing we can do. + stderr..writeln('Fatal error: ${e.error}')..writeln(e.stack); + } }; - // Throw a 404 if no route matched the request - app.after.add(errors.throwError()); + // Throw a 404 if no route matched the request. + app.after.add(() => throw new AngelHttpException.notFound()); // Handle errors when they occur, based on outgoing status code. // By default, requests will go through the 500 handler, unless @@ -45,19 +94,45 @@ configureAfter(Angel app) async { // registered. app.after.add(errors.middleware()); - // Pass AngelHttpExceptions through handler as well - await app.configure(errors); - - // Compress via GZIP - // Ideally you'll run this on a `multiserver` instance, but if not, - // feel free to knock yourself out! + // Pass AngelHttpExceptions through handler as well. // - // app.responseFinalizers.add(gzip()); + // Again, here is the error handling documentation: + // * https://github.com/angel-dart/angel/wiki/Error-Handling + // * https://github.com/angel-dart/errors + await app.configure(errors); } +/// Adds response finalizers to the application. +/// +/// These run after every request. +/// +/// See more on the request lifecycle here: +/// https://github.com/angel-dart/angel/wiki/Request-Lifecycle +configureFinalizers(Angel app) async {} + +/// Adds routes to our application. +/// +/// See the wiki for information about routing, requests, and responses: +/// * https://github.com/angel-dart/angel/wiki/Basic-Routing +/// * https://github.com/angel-dart/angel/wiki/Requests-&-Responses configureServer(Angel app) async { + // The order in which we run these plug-ins is relatively significant. + // Try not to change it. + + // Add global middleware. await configureBefore(app); + + // Typically, you want to mount controllers first, after any global middleware. await app.configure(controllers.configureServer); + + // Next, you can add any supplemental routes. await configureRoutes(app); + + // Add handlers to run after requests are handled. + // + // See the request lifecycle docs to find out why these two + // are separate: + // https://github.com/angel-dart/angel/wiki/Request-Lifecycle await configureAfter(app); + await configureFinalizers(app); } diff --git a/lib/src/services/services.dart b/lib/src/services/services.dart index 9c29925a..62307100 100644 --- a/lib/src/services/services.dart +++ b/lib/src/services/services.dart @@ -2,10 +2,16 @@ library angel.services; import 'package:angel_common/angel_common.dart'; -import 'package:mongo_dart/mongo_dart.dart'; import 'user.dart' as user; +/// Configure our application to use *services*. +/// Services must be wired to the app via `app.use`. +/// +/// They provide many benefits, such as instant REST API generation, +/// and respond to both REST and WebSockets. +/// +/// Read more here: +/// https://github.com/angel-dart/angel/wiki/Service-Basics configureServer(Angel app) async { - Db db = app.container.make(Db); - await app.configure(user.configureServer(db)); + await app.configure(user.configureServer()); } diff --git a/lib/src/services/user.dart b/lib/src/services/user.dart index b5927ab0..45f15263 100644 --- a/lib/src/services/user.dart +++ b/lib/src/services/user.dart @@ -1,16 +1,29 @@ import 'package:angel_common/angel_common.dart'; import 'package:angel_framework/hooks.dart' as hooks; import 'package:crypto/crypto.dart' show sha256; -import 'package:mongo_dart/mongo_dart.dart'; import 'package:random_string/random_string.dart' as rs; import '../models/user.dart'; import '../validators/user.dart'; export '../models/user.dart'; -configureServer(Db db) { +/// Sets up a service mounted at `api/users`. +/// +/// In the real world, you will want to hook this up to a database. +/// However, for the sake of the boilerplate, an in-memory service is used, +/// so that users are not tied into using just one database. :) +configureServer() { return (Angel app) async { - app.use('/api/users', - new TypedService(new MongoService(db.collection('users')))); + // A TypedService can be used to serialize and deserialize data to a class, somewhat like an ORM. + // + // See here: https://github.com/angel-dart/angel/wiki/TypedService + app.use('/api/users', new TypedService(new MapService())); + + // Configure hooks for the user service. + // Hooks can be used to add additional functionality, or change the behavior + // of services, and run on any service, regardless of which database you are using. + // + // If you have not already, *definitely* read the service hook documentation: + // https://github.com/angel-dart/angel/wiki/Hooks var service = app.service('api/users') as HookedService; diff --git a/lib/src/validators/user.dart b/lib/src/validators/user.dart index 1d8bf2fd..4cb72ae4 100644 --- a/lib/src/validators/user.dart +++ b/lib/src/validators/user.dart @@ -1,5 +1,9 @@ import 'package:angel_validate/angel_validate.dart'; +// Validators can be used on the server, in the browser, and even in Flutter. +// +// It is highly recommended that you read the documentation: +// https://github.com/angel-dart/validate final Validator USER = new Validator({ 'email': [isString, isNotEmpty, isEmail], 'username': [isString, isNotEmpty], diff --git a/pubspec.yaml b/pubspec.yaml index d0854e1a..8b226af3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,17 +1,21 @@ name: angel description: An easily-extensible web server framework in Dart. -publish_to: none +publish_to: none # Ensure we don't accidentally publish our private code! ;) environment: sdk: ">=1.19.0" homepage: https://github.com/angel-dart/angel dependencies: - angel_common: ^1.0.0 - angel_configuration: ^1.0.0 - angel_hot: ^1.0.0-rc.1 - angel_multiserver: ^1.0.0 + angel_common: ^1.0.0 # Bundles the most commonly-used Angel packages. + angel_configuration: ^1.0.0 # Included in `angel_common`, but also exposes a transformer + angel_hot: ^1.0.0-rc.1 # Hot-reloading support. :) + angel_multiserver: ^1.0.0 # Helpful for applications running a scale. dev_dependencies: grinder: ^0.8.0 http: ^0.11.3 test: ^0.12.13 -transformers: +transformers: + # Injects data from application configuration into Dart code. + # + # Documentation: + # https://github.com/angel-dart/configuration - angel_configuration diff --git a/test/services/users_test.dart b/test/services/users_test.dart index dbc627e8..0ffdcd2a 100644 --- a/test/services/users_test.dart +++ b/test/services/users_test.dart @@ -1,27 +1,42 @@ import 'dart:io'; import 'package:angel/angel.dart'; -import 'package:angel_framework/angel_framework.dart'; import 'package:angel_test/angel_test.dart'; import 'package:test/test.dart'; +// Angel also includes facilities to make testing easier. +// +// `package:angel_test` ships a client that can test +// both plain HTTP and WebSockets. +// +// Tests do not require your server to actually be mounted on a port, +// so they will run faster than they would in other frameworks, where you +// would have to first bind a socket, and then account for network latency. +// +// See the documentation here: +// https://github.com/angel-dart/test +// +// If you are unfamiliar with Dart's advanced testing library, you can read up +// here: +// https://github.com/dart-lang/test + main() async { - Angel app; TestClient client; setUp(() async { - app = await createServer(); + var app = await createServer(); client = await connectTo(app); }); tearDown(() async { await client.close(); - app = null; }); test('index users', () async { - final response = await client.get('/api/users'); + // Request a resource at the given path. + var response = await client.get('/api/users'); // By default, we locked this away from the Internet... + // Expect a 403 response. expect(response, hasStatus(HttpStatus.FORBIDDEN)); }); } diff --git a/tool/grind.dart b/tool/grind.dart index 1451802d..d26a26ae 100644 --- a/tool/grind.dart +++ b/tool/grind.dart @@ -1,3 +1,9 @@ +// Grinder is not part of Angel, but you may consider using it +// to run tasks, a la Gulp. +// +// See its documentation here: +// https://github.com/google/grinder.dart + import 'package:grinder/grinder.dart'; main(args) => grind(args);