Add 'packages/production/' from commit '5ac875c504cd98a5075e0316da9c153b313fd3f9'
git-subtree-dir: packages/production git-subtree-mainline:c97363d290
git-subtree-split:5ac875c504
This commit is contained in:
commit
cf2ad35d2b
14 changed files with 680 additions and 0 deletions
13
packages/production/.gitignore
vendored
Normal file
13
packages/production/.gitignore
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# See https://www.dartlang.org/guides/libraries/private-files
|
||||||
|
|
||||||
|
# Files and directories created by pub
|
||||||
|
.dart_tool/
|
||||||
|
.packages
|
||||||
|
.pub/
|
||||||
|
build/
|
||||||
|
# If you're building an application, you may want to check-in your pubspec.lock
|
||||||
|
pubspec.lock
|
||||||
|
|
||||||
|
# Directory created by dartdoc
|
||||||
|
# If you don't generate documentation locally you can remove this line.
|
||||||
|
doc/api/
|
9
packages/production/CHANGELOG.md
Normal file
9
packages/production/CHANGELOG.md
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# 1.0.0
|
||||||
|
* Support SSL/HTTP2.
|
||||||
|
* Support muting the logger via `--quiet`.
|
||||||
|
|
||||||
|
# 1.0.0-alpha.1
|
||||||
|
* Import `framework/http`.
|
||||||
|
|
||||||
|
# 1.0.0-alpha
|
||||||
|
* Initial version.
|
21
packages/production/LICENSE
Normal file
21
packages/production/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018 The Angel Framework
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
116
packages/production/README.md
Normal file
116
packages/production/README.md
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
# production
|
||||||
|
[![Pub](https://img.shields.io/pub/v/angel_production.svg)](https://pub.dartlang.org/packages/angel_production)
|
||||||
|
|
||||||
|
Helpers for concurrency, message-passing, rotating loggers, and other production functionality in Angel.
|
||||||
|
|
||||||
|
![Screenshot](screenshot.png)
|
||||||
|
|
||||||
|
This will become the de-facto way to run Angel applications in deployed environments, as it
|
||||||
|
takes care of inter-isolate communication, respawning dead processes, and other housekeeping for you automatically.
|
||||||
|
|
||||||
|
Most users will want to use the `Runner` class.
|
||||||
|
|
||||||
|
## `Runner`
|
||||||
|
`Runner` is a utility, powered by `package:args`, that is intended to be the entry point of your application.
|
||||||
|
|
||||||
|
Instantiate it as follows, and your file will become a command-line executable that spawns multiple instances of your
|
||||||
|
application:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:isolate';
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:angel_production/angel_production.dart';
|
||||||
|
|
||||||
|
main(List<String> args) => Runner('example', configureServer).run(args);
|
||||||
|
|
||||||
|
Future configureServer(Angel app) async {
|
||||||
|
app.get('/', (req, res) => 'Hello, production world!');
|
||||||
|
|
||||||
|
app.get('/crash', (req, res) {
|
||||||
|
// We'll crash this instance deliberately, but the Runner will auto-respawn for us.
|
||||||
|
Timer(const Duration(seconds: 3), Isolate.current.kill);
|
||||||
|
return 'Crashing in 3s...';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`Runner` will automatically re-spawn crashed instances, unless `--no-respawn` is passed. This can prevent
|
||||||
|
your server from entirely going down at the first error, and adds a layer of fault tolerance to your
|
||||||
|
infrastructure.
|
||||||
|
|
||||||
|
When combined with `systemd`, deploying Angel applications on Linux can be very simple.
|
||||||
|
|
||||||
|
## Message Passing
|
||||||
|
The `Runner` class uses [`package:pub_sub`](https://github.com/thosakwe/pub_sub) to coordinate
|
||||||
|
message passing between isolates.
|
||||||
|
|
||||||
|
When one isolate sends a message, all other isolates will
|
||||||
|
receive the same message, except for the isolate that sent it.
|
||||||
|
|
||||||
|
It is injected into your application's `Container` as
|
||||||
|
`pub_sub.Client`, so you can use it as follows:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// Use the injected `pub_sub.Client` to send messages.
|
||||||
|
var client = app.container.make<pub_sub.Client>();
|
||||||
|
|
||||||
|
// We can listen for an event to perform some behavior.
|
||||||
|
//
|
||||||
|
// Here, we use message passing to synchronize some common state.
|
||||||
|
var onGreetingChanged = await client.subscribe('user_upgraded');
|
||||||
|
onGreetingChanged
|
||||||
|
.cast<User>()
|
||||||
|
.listen((user) {
|
||||||
|
// Do something...
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run-time Metadata
|
||||||
|
At run-time, you may want to know information about the currently-running instance,
|
||||||
|
for example, which number instance. For this, the `InstanceInfo` class is injected
|
||||||
|
into each instance:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
var instanceInfo = app.container.make<InstanceInfo>();
|
||||||
|
print('This is instance #${instanceInfo.id}');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Command-line Options
|
||||||
|
The `Runner` class supplies options like the following:
|
||||||
|
|
||||||
|
```
|
||||||
|
Tobes-MacBook-Air:production thosakwe$ dart example/main.dart --help
|
||||||
|
____________ ________________________
|
||||||
|
___ |__ | / /_ ____/__ ____/__ /
|
||||||
|
__ /| |_ |/ /_ / __ __ __/ __ /
|
||||||
|
_ ___ | /| / / /_/ / _ /___ _ /___
|
||||||
|
/_/ |_/_/ |_/ ____/ /_____/ /_____/
|
||||||
|
|
||||||
|
A batteries-included, full-featured, full-stack framework in Dart.
|
||||||
|
|
||||||
|
https://angel-dart.github.io
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h, --help Print this help information.
|
||||||
|
--[no-]respawn Automatically respawn crashed application instances.
|
||||||
|
(defaults to on)
|
||||||
|
|
||||||
|
--use-zone Create a new Zone for each request.
|
||||||
|
--quiet Completely mute logging.
|
||||||
|
--ssl Listen for HTTPS instead of HTTP.
|
||||||
|
--http2 Listen for HTTP/2 instead of HTTP/1.1.
|
||||||
|
-a, --address The address to listen on.
|
||||||
|
(defaults to "127.0.0.1")
|
||||||
|
|
||||||
|
-j, --concurrency The number of isolates to spawn.
|
||||||
|
(defaults to "4")
|
||||||
|
|
||||||
|
-p, --port The port to listen on.
|
||||||
|
(defaults to "3000")
|
||||||
|
|
||||||
|
--certificate-file The PEM certificate file to read.
|
||||||
|
--certificate-password The PEM certificate file password.
|
||||||
|
--key-file The PEM key file to read.
|
||||||
|
--key-password The PEM key file password.
|
||||||
|
```
|
4
packages/production/analysis_options.yaml
Normal file
4
packages/production/analysis_options.yaml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
include: package:pedantic/analysis_options.yaml
|
||||||
|
analyzer:
|
||||||
|
strong-mode:
|
||||||
|
implicit-casts: false
|
29
packages/production/dev.key
Normal file
29
packages/production/dev.key
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||||
|
MIIE5DAcBgoqhkiG9w0BDAEBMA4ECL7L6rj6uEHGAgIIAASCBMLbucyfqAkgCbhP
|
||||||
|
xNSHYllPMAv/dsIjtnsBwepCXPGkCBCuOAw/2FaCHjN9hBqL5V7fkrKeaemhm2YE
|
||||||
|
ycPtlHJYPDf3kEkyMjdZ9rIY6kePGfQizs2uJPcXj4YPyQ4HsfVXpOicKfQrouf5
|
||||||
|
Mze9bGzeMN065q3iP4dYUMwHAyZYteXCsanQNHlqvsWli0W+H8St8fdsXefZhnv1
|
||||||
|
qVatKWdNdWQ9t5MuljgNU2Vv56sHKEYXI0yLxk2QUMk8KlJfnmt8foYUsnPUXHmc
|
||||||
|
gIjLKwwVkpdololnEHSNu0cEOUPowjgJru+uMpn7vdNl7TPEQ9jbEgdNg4JwoYzU
|
||||||
|
0nao8WzjaSp7kzvZz0VFwKnk5AjstGvvuAWckADdq23QElbn/mF7AG1m/TBpYxzF
|
||||||
|
gTt37UdndS/AcvVznWVVrRP5iTSIawdIwvqI4s7rqsoE0GCcak+RhchgAz2gWKkS
|
||||||
|
oODUo0JL6pPVbJ3l4ebbaO6c99nDVc8dViPtc1EkStJEJ2O4kI4xgLSCr4Y9ahKn
|
||||||
|
oAaoSkX7Xxq3aQm+BzqSpLjdGL8atsqR/YVOIHYIl3gThvP0NfZGx1xHyvO5mCdZ
|
||||||
|
kHxSA7tKWxauZ3eQ2clbnzeRsl4El0WMHy/5K1ovene4v7sunmoXVtghBC8hK6eh
|
||||||
|
zMO9orex2PNQ/VQC7HCvtytunOVx1lkSBoNo7hR70igg6rW9H7UyoAoBOwMpT1xa
|
||||||
|
J6V62nqruTKOqFNfur7aHJGpHGtDb5/ickHeYCyPTvmGp67u4wChzKReeg02oECe
|
||||||
|
d1E5FKAcIa8s9TVOB6Z+HvTRNQZu2PsI6TJnjQRowvY9DAHiWTlJZBBY/pko3hxX
|
||||||
|
TsIeybpvRdEHpDWv86/iqtw1hv9CUxS/8ZTWUgBo+osShHW79FeDASr9FC4/Zn76
|
||||||
|
ZDERTgV4YWlW/klVWcG2lFo7jix+OPXAB+ZQavLhlN1xdWBcIz1AUWjAM4hdPylW
|
||||||
|
HCX4PB9CQIPl2E7F+Y2p6nMcMWSJVBi5UIH7E9LfaBguXSzMmTk2Fw5p1aOQ6wfN
|
||||||
|
goVAMVwi8ppAVs741PfHdZ295xMmK/1LCxz5DeAdD/tsA/SYfT753GotioDuC7im
|
||||||
|
EyJ5JyvTr5I6RFFBuqt3NlUb3Hp16wP3B2x9DZiB6jxr0l341/NHgsyeBXkuIy9j
|
||||||
|
ON2mvpBPCJhS8kgWo3G0UyyKnx64tcgpGuSvZhGwPz843B6AbYyE6pMRfSWRMkMS
|
||||||
|
YZYa+VNKhR4ixdj07ocFZEWLVjCH7kxkE8JZXKt8jKYmkWd0lS1QVjgaKlO6lRa3
|
||||||
|
q6SPJkhW6pvqobvcqVNXwi1XuzpZeEbuh0B7OTekFTTxx5g9XeDl56M8SVQ1KEhT
|
||||||
|
Q1t7H2Nba18WCB7cf+6PN0F0K0Jz1Kq7ZWaqEI/grX1m4RQuvNF5807sB/QKMO/Z
|
||||||
|
Gz3NXvHg5xTJRd/567lxPGkor0cE7qD1EZfmJ2HrBYXQ91bhgA7LToBuMZo6ZRXH
|
||||||
|
QfsanjbP4FPLMiGdQigLjj3A35L/f4sQOOVac/sRaFnm7pzcxsMvyVU/YtvGcjYE
|
||||||
|
xaOOVnamg661Wo0wksXoDjeSz/JIyyKO3Gwp1FSm2wGLjjy/Ehmqcqy8rvHuf07w
|
||||||
|
AUukhVtTNn4=
|
||||||
|
-----END ENCRYPTED PRIVATE KEY-----
|
57
packages/production/dev.pem
Normal file
57
packages/production/dev.pem
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDKTCCAhGgAwIBAgIJAOWmjTS+OnTEMA0GCSqGSIb3DQEBCwUAMBcxFTATBgNV
|
||||||
|
BAMMDGludGVybWVkaWF0ZTAeFw0xNTA1MTgwOTAwNDBaFw0yMzA4MDQwOTAwNDBa
|
||||||
|
MBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
|
||||||
|
AQoCggEBALlcwQJuzd+xH8QFgfJSn5tRlvhkldSX98cE7NiA602NBbnAVyUrkRXq
|
||||||
|
Ni75lgt0kwjYfA9z674m8WSVbgpLPintPCla9CYky1TH0keIs8Rz6cGWHryWEHiu
|
||||||
|
EDuljQynu2b3sAFuHu9nfWurbJwZnFakBKpdQ9m4EyOZCHC/jHYY7HacKSXg1Cki
|
||||||
|
we2ca0BWDrcqy8kLy0dZ5oC6IZG8O8drAK8f3f44CRYw59D3sOKBrKXaabpvyEcb
|
||||||
|
N7Wk2HDBVwHpUJo1reVwtbM8dhqQayYSD8oXnGpP3RQNu/e2rzlXRyq/BfcDY1JI
|
||||||
|
7TbC4t/7/N4EcPSpGsTcSOC9A7FpzvECAwEAAaN7MHkwCQYDVR0TBAIwADAsBglg
|
||||||
|
hkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0O
|
||||||
|
BBYEFCnwiEMMFZh7NhCr+qA8K0w4Q+AOMB8GA1UdIwQYMBaAFB0h1Evsaw2vfrmS
|
||||||
|
YuoCTmC4EE6ZMA0GCSqGSIb3DQEBCwUAA4IBAQAcFmHMaXRxyoNaeOowQ6iQWoZd
|
||||||
|
AUbvG7SHr7I6Pi2aqdqofsKWts7Ytm5WsS0M2nN+sW504houu0iCPeJJX8RQw2q4
|
||||||
|
CCcNOs9IXk+2uMzlpocHpv+yYoUiD5DxgWh7eghQMLyMpf8FX3Gy4VazeuXznHOM
|
||||||
|
4gE4L417xkDzYOzqVTp0FTyAPUv6G2euhNCD6TMru9REcRhYul+K9kocjA5tt2KG
|
||||||
|
MH6y28LXbLyq4YJUxSUU9gY/xlnbbZS48KDqEcdYC9zjW9nQ0qS+XQuQuFIcwjJ5
|
||||||
|
V4kAUYxDu6FoTpyQjgsrmBbZlKNxH7Nj4NDlcdJhp/zeSKHqWa5hSWjjKIxp
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDAjCCAeqgAwIBAgIJAOWmjTS+OnTDMA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV
|
||||||
|
BAMMDXJvb3RhdXRob3JpdHkwHhcNMTUwNTE4MDkwMDQwWhcNMjMwODA0MDkwMDQw
|
||||||
|
WjAXMRUwEwYDVQQDDAxpbnRlcm1lZGlhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IB
|
||||||
|
DwAwggEKAoIBAQDSrAO1CoPvUllgLOzDm5nG0skDF7vh1DUgAIDVGz0ecD0JFbQx
|
||||||
|
EF79pju/6MbtpTW2FYvRp11t/G7rGtX923ybOHY/1MNFQrdIvPlO1VV7IGKjoMwP
|
||||||
|
DNeb0fIGjHoE9QxaDxR8NX8xQbItpsw+TUtRfc9SLkR+jaYJfVRoM21BOncZbSHE
|
||||||
|
YKiZlEbpecB/+EtwVpgvl+8mPD5U07Fi4fp/lza3WXInXQPyiTVllIEJCt4PKmlu
|
||||||
|
MocNaJOW38bysL7i0PzDpVZtOxLHOTaW68yF3FckIHNCaA7k1ABEEEegjFMmIao7
|
||||||
|
B9w7A0jvr4jZVvNmui5Djjn+oJxwEVVgyf8LAgMBAAGjUDBOMB0GA1UdDgQWBBQd
|
||||||
|
IdRL7GsNr365kmLqAk5guBBOmTAfBgNVHSMEGDAWgBRk81s9d0ZbiZhh44KckwPb
|
||||||
|
oTc0XzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBZQTK0plfdB5PC
|
||||||
|
cC5icut4EmrByJa1RbU7ayuEE70e7hla6KVmVjVdCBGltI4jBYwfhKbRItHiAJ/8
|
||||||
|
x+XZKBG8DLPFuDb7lAa1ObhAYF7YThUFPQYaBhfzKcWrdmWDBFpvNv6E0Mm364dZ
|
||||||
|
e7Yxmbe5S4agkYPoxEzgEYmcUk9jbjdR6eTbs8laG169ljrECXfEU9RiAcqz5iSX
|
||||||
|
NLSewqB47hn3B9qgKcQn+PsgO2j7M+rfklhNgeGJeWmy7j6clSOuCsIjWHU0RLQ4
|
||||||
|
0W3SB/rpEAJ7fgQbYUPTIUNALSOWi/o1tDX2mXPRjBoxqAv7I+vYk1lZPmSzkyRh
|
||||||
|
FKvRDxsW
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDAzCCAeugAwIBAgIJAJ0MomS4Ck+8MA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV
|
||||||
|
BAMMDXJvb3RhdXRob3JpdHkwHhcNMTUwNTE4MDkwMDQwWhcNMjMwODA0MDkwMDQw
|
||||||
|
WjAYMRYwFAYDVQQDDA1yb290YXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOC
|
||||||
|
AQ8AMIIBCgKCAQEAts1ijtBV92S2cOvpUMOSTp9c6A34nIGr0T5Nhz6XiqRVT+gv
|
||||||
|
dQgmkdKJQjbvR60y6jzltYFsI2MpGVXY8h/oAL81D/k7PDB2aREgyBfTPAhBHyGw
|
||||||
|
siR+2xYt5b/Zs99q5RdRqQNzNpLPJriIKvUsRyQWy1UiG2s7pRXQeA8qB0XtJdCj
|
||||||
|
kFIi+G2bDsaffspGeDOCqt7t+yqvRXfSES0c/l7DIHaiMbbp4//ZNML3RNgAjPz2
|
||||||
|
hCezZ+wOYajOIyoSPK8IgICrhYFYxvgWxwbLDBEfC5B3jOQsySe10GoRAKZz1gBV
|
||||||
|
DmgReu81tYJmdgkc9zknnQtIFdA0ex+GvZlfWQIDAQABo1AwTjAdBgNVHQ4EFgQU
|
||||||
|
ZPNbPXdGW4mYYeOCnJMD26E3NF8wHwYDVR0jBBgwFoAUZPNbPXdGW4mYYeOCnJMD
|
||||||
|
26E3NF8wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEATzkZ97K777uZ
|
||||||
|
lQcduNX3ey4IbCiEzFA2zO5Blj+ilfIwNbZXNOgm/lqNvVGDYs6J1apJJe30vL3X
|
||||||
|
J+t2zsZWzzQzb9uIU37zYemt6m0fHrSrx/iy5lGNqt3HMfqEcOqSCOIK3PCTMz2/
|
||||||
|
uyGe1iw33PVeWsm1JUybQ9IrU/huJjbgOHU4wab+8SJCM49ipArp68Fr6j4lcEaE
|
||||||
|
4rfRg1ZsvxiOyUB3qPn6wyL/JB8kOJ+QCBe498376eaem8AEFk0kQRh6hDaWtq/k
|
||||||
|
t6IIXQLjx+EBDVP/veK0UnVhKRP8YTOoV8ZiG1NcdlJmX/Uk7iAfevP7CkBfSN8W
|
||||||
|
r6AL284qtw==
|
||||||
|
-----END CERTIFICATE-----
|
46
packages/production/example/main.dart
Normal file
46
packages/production/example/main.dart
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:isolate';
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:angel_production/angel_production.dart';
|
||||||
|
import 'package:pub_sub/pub_sub.dart' as pub_sub;
|
||||||
|
|
||||||
|
main(List<String> args) => Runner('example', configureServer).run(args);
|
||||||
|
|
||||||
|
Future configureServer(Angel app) async {
|
||||||
|
// Use the injected `pub_sub.Client` to send messages.
|
||||||
|
var client = app.container.make<pub_sub.Client>();
|
||||||
|
var greeting = 'Hello! This is the default greeting.';
|
||||||
|
|
||||||
|
// We can listen for an event to perform some behavior.
|
||||||
|
//
|
||||||
|
// Here, we use message passing to synchronize some common state.
|
||||||
|
var onGreetingChanged = await client.subscribe('greeting_changed');
|
||||||
|
onGreetingChanged
|
||||||
|
.cast<String>()
|
||||||
|
.listen((newGreeting) => greeting = newGreeting);
|
||||||
|
|
||||||
|
// Add some routes...
|
||||||
|
app.get('/', (req, res) => 'Hello, production world!');
|
||||||
|
|
||||||
|
app.get('/404', (req, res) {
|
||||||
|
res.statusCode = 404;
|
||||||
|
return res.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create some routes to demonstrate message passing.
|
||||||
|
app.get('/greeting', (req, res) => greeting);
|
||||||
|
|
||||||
|
// This route will push a new value for `greeting`.
|
||||||
|
app.get('/change_greeting/:newGreeting', (req, res) {
|
||||||
|
greeting = req.params['newGreeting'] as String;
|
||||||
|
client.publish('greeting_changed', greeting);
|
||||||
|
return 'Changed greeting -> $greeting';
|
||||||
|
});
|
||||||
|
|
||||||
|
// The `Runner` helps with fault tolerance.
|
||||||
|
app.get('/crash', (req, res) {
|
||||||
|
// We'll crash this instance deliberately, but the Runner will auto-respawn for us.
|
||||||
|
Timer(const Duration(seconds: 3), Isolate.current.kill);
|
||||||
|
return 'Crashing in 3s...';
|
||||||
|
});
|
||||||
|
}
|
3
packages/production/lib/angel_production.dart
Normal file
3
packages/production/lib/angel_production.dart
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export 'src/instance_info.dart';
|
||||||
|
export 'src/options.dart';
|
||||||
|
export 'src/runner.dart';
|
6
packages/production/lib/src/instance_info.dart
Normal file
6
packages/production/lib/src/instance_info.dart
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
/// Information about the currently-running instance.
|
||||||
|
class InstanceInfo {
|
||||||
|
final int id;
|
||||||
|
|
||||||
|
const InstanceInfo({this.id});
|
||||||
|
}
|
79
packages/production/lib/src/options.dart
Normal file
79
packages/production/lib/src/options.dart
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:args/args.dart';
|
||||||
|
|
||||||
|
class RunnerOptions {
|
||||||
|
static final ArgParser argParser = ArgParser()
|
||||||
|
..addFlag('help',
|
||||||
|
abbr: 'h', help: 'Print this help information.', negatable: false)
|
||||||
|
..addFlag('respawn',
|
||||||
|
help: 'Automatically respawn crashed application instances.',
|
||||||
|
defaultsTo: true,
|
||||||
|
negatable: true)
|
||||||
|
..addFlag('use-zone',
|
||||||
|
negatable: false, help: 'Create a new Zone for each request.')
|
||||||
|
..addFlag('quiet', negatable: false, help: 'Completely mute logging.')
|
||||||
|
..addFlag('ssl',
|
||||||
|
negatable: false, help: 'Listen for HTTPS instead of HTTP.')
|
||||||
|
..addFlag('http2',
|
||||||
|
negatable: false, help: 'Listen for HTTP/2 instead of HTTP/1.1.')
|
||||||
|
..addOption('address',
|
||||||
|
abbr: 'a', defaultsTo: '127.0.0.1', help: 'The address to listen on.')
|
||||||
|
..addOption('concurrency',
|
||||||
|
abbr: 'j',
|
||||||
|
defaultsTo: Platform.numberOfProcessors.toString(),
|
||||||
|
help: 'The number of isolates to spawn.')
|
||||||
|
..addOption('port',
|
||||||
|
abbr: 'p', defaultsTo: '3000', help: 'The port to listen on.')
|
||||||
|
..addOption('certificate-file', help: 'The PEM certificate file to read.')
|
||||||
|
..addOption('certificate-password',
|
||||||
|
help: 'The PEM certificate file password.')
|
||||||
|
..addOption('key-file', help: 'The PEM key file to read.')
|
||||||
|
..addOption('key-password', help: 'The PEM key file password.');
|
||||||
|
|
||||||
|
final String hostname,
|
||||||
|
certificateFile,
|
||||||
|
keyFile,
|
||||||
|
certificatePassword,
|
||||||
|
keyPassword;
|
||||||
|
final int concurrency, port;
|
||||||
|
final bool useZone, respawn, quiet, ssl, http2;
|
||||||
|
|
||||||
|
RunnerOptions(
|
||||||
|
{this.hostname = '127.0.0.1',
|
||||||
|
this.port = 3000,
|
||||||
|
this.concurrency = 1,
|
||||||
|
this.useZone = false,
|
||||||
|
this.respawn = true,
|
||||||
|
this.quiet = false,
|
||||||
|
this.certificateFile,
|
||||||
|
this.keyFile,
|
||||||
|
this.ssl = false,
|
||||||
|
this.http2 = false,
|
||||||
|
this.certificatePassword,
|
||||||
|
this.keyPassword});
|
||||||
|
|
||||||
|
factory RunnerOptions.fromArgResults(ArgResults argResults) {
|
||||||
|
return RunnerOptions(
|
||||||
|
hostname: argResults['address'] as String,
|
||||||
|
port: int.parse(argResults['port'] as String),
|
||||||
|
concurrency: int.parse(argResults['concurrency'] as String),
|
||||||
|
useZone: argResults['use-zone'] as bool,
|
||||||
|
respawn: argResults['respawn'] as bool,
|
||||||
|
quiet: argResults['quiet'] as bool,
|
||||||
|
certificateFile: argResults.wasParsed('certificate-file')
|
||||||
|
? argResults['certificate-file'] as String
|
||||||
|
: null,
|
||||||
|
keyFile: argResults.wasParsed('key-file')
|
||||||
|
? argResults['key-file'] as String
|
||||||
|
: null,
|
||||||
|
ssl: argResults['ssl'] as bool,
|
||||||
|
http2: argResults['http2'] as bool,
|
||||||
|
certificatePassword: argResults.wasParsed('certificate-password')
|
||||||
|
? argResults['certificate-password'] as String
|
||||||
|
: null,
|
||||||
|
keyPassword: argResults.wasParsed('key-password')
|
||||||
|
? argResults['key-password'] as String
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
281
packages/production/lib/src/runner.dart
Normal file
281
packages/production/lib/src/runner.dart
Normal file
|
@ -0,0 +1,281 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:isolate';
|
||||||
|
import 'package:angel_container/angel_container.dart';
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:angel_framework/http.dart';
|
||||||
|
import 'package:angel_framework/http2.dart';
|
||||||
|
import 'package:args/args.dart';
|
||||||
|
import 'package:io/ansi.dart';
|
||||||
|
import 'package:io/io.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:pub_sub/isolate.dart' as pub_sub;
|
||||||
|
import 'package:pub_sub/pub_sub.dart' as pub_sub;
|
||||||
|
import 'instance_info.dart';
|
||||||
|
import 'options.dart';
|
||||||
|
|
||||||
|
/// A command-line utility for easier running of multiple instances of an Angel application.
|
||||||
|
///
|
||||||
|
/// Makes it easy to do things like configure SSL, log messages, and send messages between
|
||||||
|
/// all running instances.
|
||||||
|
class Runner {
|
||||||
|
final String name;
|
||||||
|
final AngelConfigurer configureServer;
|
||||||
|
final Reflector reflector;
|
||||||
|
|
||||||
|
Runner(this.name, this.configureServer,
|
||||||
|
{this.reflector = const EmptyReflector()});
|
||||||
|
|
||||||
|
static const String asciiArt = '''
|
||||||
|
____________ ________________________
|
||||||
|
___ |__ | / /_ ____/__ ____/__ /
|
||||||
|
__ /| |_ |/ /_ / __ __ __/ __ /
|
||||||
|
_ ___ | /| / / /_/ / _ /___ _ /___
|
||||||
|
/_/ |_/_/ |_/ \____/ /_____/ /_____/
|
||||||
|
|
||||||
|
''';
|
||||||
|
|
||||||
|
static void handleLogRecord(LogRecord record, RunnerOptions options) {
|
||||||
|
if (options.quiet) return;
|
||||||
|
var code = chooseLogColor(record.level);
|
||||||
|
|
||||||
|
if (record.error == null) print(code.wrap(record.toString()));
|
||||||
|
|
||||||
|
if (record.error != null) {
|
||||||
|
var err = record.error;
|
||||||
|
if (err is AngelHttpException && err.statusCode != 500) return;
|
||||||
|
print(code.wrap(record.toString() + '\n'));
|
||||||
|
print(code.wrap(err.toString()));
|
||||||
|
|
||||||
|
if (record.stackTrace != null) {
|
||||||
|
print(code.wrap(record.stackTrace.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Chooses a color based on the logger [level].
|
||||||
|
static AnsiCode chooseLogColor(Level level) {
|
||||||
|
if (level == Level.SHOUT)
|
||||||
|
return backgroundRed;
|
||||||
|
else if (level == Level.SEVERE)
|
||||||
|
return red;
|
||||||
|
else if (level == Level.WARNING)
|
||||||
|
return yellow;
|
||||||
|
else if (level == Level.INFO)
|
||||||
|
return cyan;
|
||||||
|
else if (level == Level.FINER || level == Level.FINEST) return lightGray;
|
||||||
|
return resetAll;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawns a new instance of the application in a separate isolate.
|
||||||
|
///
|
||||||
|
/// If the command-line arguments permit, then the instance will be respawned on crashes.
|
||||||
|
///
|
||||||
|
/// The returned [Future] completes when the application instance exits.
|
||||||
|
///
|
||||||
|
/// If respawning is enabled, the [Future] will *never* complete.
|
||||||
|
Future spawnIsolate(int id, RunnerOptions options, SendPort pubSubSendPort) {
|
||||||
|
return _spawnIsolate(id, Completer(), options, pubSubSendPort);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _spawnIsolate(
|
||||||
|
int id, Completer c, RunnerOptions options, SendPort pubSubSendPort) {
|
||||||
|
var onLogRecord = ReceivePort();
|
||||||
|
var onExit = ReceivePort();
|
||||||
|
var onError = ReceivePort();
|
||||||
|
var runnerArgs = _RunnerArgs(name, configureServer, options, reflector,
|
||||||
|
onLogRecord.sendPort, pubSubSendPort);
|
||||||
|
var argsWithId = _RunnerArgsWithId(id, runnerArgs);
|
||||||
|
|
||||||
|
Isolate.spawn(isolateMain, argsWithId,
|
||||||
|
onExit: onExit.sendPort,
|
||||||
|
onError: onError.sendPort,
|
||||||
|
errorsAreFatal: true && false)
|
||||||
|
.then((isolate) {})
|
||||||
|
.catchError(c.completeError);
|
||||||
|
|
||||||
|
onLogRecord.listen((msg) => handleLogRecord(msg as LogRecord, options));
|
||||||
|
|
||||||
|
onError.listen((msg) {
|
||||||
|
if (msg is List) {
|
||||||
|
var e = msg[0], st = StackTrace.fromString(msg[1].toString());
|
||||||
|
handleLogRecord(
|
||||||
|
LogRecord(
|
||||||
|
Level.SEVERE, 'Fatal error', runnerArgs.loggerName, e, st),
|
||||||
|
options);
|
||||||
|
} else {
|
||||||
|
handleLogRecord(
|
||||||
|
LogRecord(Level.SEVERE, 'Fatal error', runnerArgs.loggerName, msg),
|
||||||
|
options);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onExit.listen((_) {
|
||||||
|
if (options.respawn) {
|
||||||
|
handleLogRecord(
|
||||||
|
LogRecord(
|
||||||
|
Level.WARNING,
|
||||||
|
'Instance #$id at ${DateTime.now()} crashed. Respawning immediately...',
|
||||||
|
runnerArgs.loggerName),
|
||||||
|
options);
|
||||||
|
_spawnIsolate(id, c, options, pubSubSendPort);
|
||||||
|
} else {
|
||||||
|
c.complete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return c.future
|
||||||
|
.whenComplete(onExit.close)
|
||||||
|
.whenComplete(onError.close)
|
||||||
|
.whenComplete(onLogRecord.close);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Starts a number of isolates, running identical instances of an Angel application.
|
||||||
|
Future run(List<String> args) async {
|
||||||
|
pub_sub.Server server;
|
||||||
|
|
||||||
|
try {
|
||||||
|
var argResults = RunnerOptions.argParser.parse(args);
|
||||||
|
var options = RunnerOptions.fromArgResults(argResults);
|
||||||
|
|
||||||
|
if (options.ssl || options.http2) {
|
||||||
|
if (options.certificateFile == null) {
|
||||||
|
throw ArgParserException('Missing --certificate-file option.');
|
||||||
|
} else if (options.keyFile == null) {
|
||||||
|
throw ArgParserException('Missing --key-file option.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print(darkGray.wrap(asciiArt.trim() +
|
||||||
|
'\n\n' +
|
||||||
|
"A batteries-included, full-featured, full-stack framework in Dart." +
|
||||||
|
'\n\n' +
|
||||||
|
'https://angel-dart.github.io\n'));
|
||||||
|
|
||||||
|
if (argResults['help'] == true) {
|
||||||
|
stdout..writeln('Options:')..writeln(RunnerOptions.argParser.usage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
print('Starting `${name}` application...');
|
||||||
|
|
||||||
|
var adapter = pub_sub.IsolateAdapter();
|
||||||
|
server = pub_sub.Server([adapter]);
|
||||||
|
|
||||||
|
// Register clients
|
||||||
|
for (int i = 0; i < Platform.numberOfProcessors; i++) {
|
||||||
|
server.registerClient(pub_sub.ClientInfo('client$i'));
|
||||||
|
}
|
||||||
|
|
||||||
|
server.start();
|
||||||
|
|
||||||
|
await Future.wait(List.generate(options.concurrency,
|
||||||
|
(id) => spawnIsolate(id, options, adapter.receivePort.sendPort)));
|
||||||
|
} on ArgParserException catch (e) {
|
||||||
|
stderr
|
||||||
|
..writeln(red.wrap(e.message))
|
||||||
|
..writeln()
|
||||||
|
..writeln(red.wrap('Options:'))
|
||||||
|
..writeln(red.wrap(RunnerOptions.argParser.usage));
|
||||||
|
exitCode = ExitCode.usage.code;
|
||||||
|
} catch (e, st) {
|
||||||
|
stderr
|
||||||
|
..writeln(red.wrap('fatal error: $e'))
|
||||||
|
..writeln(red.wrap(st.toString()));
|
||||||
|
exitCode = 1;
|
||||||
|
} finally {
|
||||||
|
await server?.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void isolateMain(_RunnerArgsWithId argsWithId) {
|
||||||
|
var args = argsWithId.args;
|
||||||
|
hierarchicalLoggingEnabled = true;
|
||||||
|
|
||||||
|
var zone = Zone.current.fork(specification: ZoneSpecification(
|
||||||
|
print: (self, parent, zone, msg) {
|
||||||
|
args.loggingSendPort.send(LogRecord(Level.INFO, msg, args.loggerName));
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
zone.run(() async {
|
||||||
|
var client =
|
||||||
|
pub_sub.IsolateClient('client${argsWithId.id}', args.pubSubSendPort);
|
||||||
|
|
||||||
|
var app = Angel(reflector: args.reflector)
|
||||||
|
..container.registerSingleton<pub_sub.Client>(client)
|
||||||
|
..container.registerSingleton(InstanceInfo(id: argsWithId.id));
|
||||||
|
|
||||||
|
app.shutdownHooks.add((_) => client.close());
|
||||||
|
|
||||||
|
await app.configure(args.configureServer);
|
||||||
|
|
||||||
|
if (app.logger == null) {
|
||||||
|
app.logger = Logger(args.loggerName)
|
||||||
|
..onRecord.listen((rec) => Runner.handleLogRecord(rec, args.options));
|
||||||
|
}
|
||||||
|
|
||||||
|
AngelHttp http;
|
||||||
|
SecurityContext securityContext;
|
||||||
|
Uri serverUrl;
|
||||||
|
|
||||||
|
if (args.options.ssl || args.options.http2) {
|
||||||
|
securityContext = SecurityContext();
|
||||||
|
securityContext.useCertificateChain(args.options.certificateFile,
|
||||||
|
password: args.options.certificatePassword);
|
||||||
|
securityContext.usePrivateKey(args.options.keyFile,
|
||||||
|
password: args.options.keyPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.options.ssl) {
|
||||||
|
http = AngelHttp.custom(app, startSharedSecure(securityContext),
|
||||||
|
useZone: args.options.useZone);
|
||||||
|
} else {
|
||||||
|
http =
|
||||||
|
AngelHttp.custom(app, startShared, useZone: args.options.useZone);
|
||||||
|
}
|
||||||
|
|
||||||
|
Driver driver;
|
||||||
|
|
||||||
|
if (args.options.http2) {
|
||||||
|
securityContext.setAlpnProtocols(['h2'], true);
|
||||||
|
var http2 = AngelHttp2.custom(app, securityContext, startSharedHttp2,
|
||||||
|
useZone: args.options.useZone);
|
||||||
|
http2.onHttp1.listen(http.handleRequest);
|
||||||
|
driver = http2;
|
||||||
|
} else {
|
||||||
|
driver = http;
|
||||||
|
}
|
||||||
|
|
||||||
|
await driver.startServer(args.options.hostname, args.options.port);
|
||||||
|
serverUrl = driver.uri;
|
||||||
|
if (args.options.ssl || args.options.http2)
|
||||||
|
serverUrl = serverUrl.replace(scheme: 'https');
|
||||||
|
print('Instance #${argsWithId.id} listening at $serverUrl');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RunnerArgsWithId {
|
||||||
|
final int id;
|
||||||
|
final _RunnerArgs args;
|
||||||
|
|
||||||
|
_RunnerArgsWithId(this.id, this.args);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RunnerArgs {
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
final AngelConfigurer configureServer;
|
||||||
|
|
||||||
|
final RunnerOptions options;
|
||||||
|
|
||||||
|
final Reflector reflector;
|
||||||
|
|
||||||
|
final SendPort loggingSendPort, pubSubSendPort;
|
||||||
|
|
||||||
|
_RunnerArgs(this.name, this.configureServer, this.options, this.reflector,
|
||||||
|
this.loggingSendPort, this.pubSubSendPort);
|
||||||
|
|
||||||
|
String get loggerName => name;
|
||||||
|
}
|
16
packages/production/pubspec.yaml
Normal file
16
packages/production/pubspec.yaml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
name: angel_production
|
||||||
|
version: 1.0.0
|
||||||
|
description: Helpers for concurrency, message-passing, rotating loggers, and other production functionality in Angel.
|
||||||
|
author: Tobe O <thosakwe@gmail.com>
|
||||||
|
homepage: https://github.com/angel-dart/production
|
||||||
|
environment:
|
||||||
|
sdk: ">=2.0.0-dev <3.0.0"
|
||||||
|
dependencies:
|
||||||
|
angel_container: ^1.0.0-alpha
|
||||||
|
angel_framework: ^2.0.0-alpha
|
||||||
|
args: ^1.0.0
|
||||||
|
io: ^0.3.2
|
||||||
|
logging: ^0.11.3
|
||||||
|
pub_sub: ^2.0.0
|
||||||
|
dev_dependencies:
|
||||||
|
pedantic: ^1.0.0
|
BIN
packages/production/screenshot.png
Normal file
BIN
packages/production/screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 79 KiB |
Loading…
Reference in a new issue