swisspost / gateleen Goto Github PK
View Code? Open in Web Editor NEWGateleen is a RESTful middleware toolkit for building API gateways
License: Other
Gateleen is a RESTful middleware toolkit for building API gateways
License: Other
Connection header (e.g Connection: close) set by proxy is forwarded to backends by gateleen.
This is a wrong behaviour according to https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html:
14.10 Connection
The Connection general-header field allows the sender to specify options that are desired for that particular connection and MUST NOT be communicated by proxies over further connections.
This has for effect to sporadically (but often) corrupt the connections between gateleen and backends. Logged error was "Connection was closed" and 503 error returned to client.
As workaround we can clear the header in routing rules with:
"staticHeaders": {
"connection": ""
}
The MonitoringHandler class provides methods to access the queueing functionality. Queue functionality is provided by the Redisques module and can be accessed through the EventBus.
Address.redisquesAddress().
In MonitoringHandler, the queueing functionality is directly accessed via the RedisClient. See example below:
redisClient.llen(queueName, reply -> {
if(reply.failed()){
log.error("Error gathering queue size for queue '" + queue + "'");
} else {
final long count = reply.result();
vertx.eventBus().publish(Address.monitoringAddress(), new JsonObject().put(METRIC_NAME, prefix + LAST_USED_QUEUE_SIZE_METRIC).put(METRIC_ACTION, "update").put("n", count));
}
});
This direct database access through the RedisClient should be omitted, because the correct way would be to use the Redisques module.
Maybe the Redisques module has to be extended to provide the needed information.
In order to be able to deploy artifact not only to local repository, we have to add support for deploying artifact to a given repository.
Example:
Local Repository:
gradle clean install -Prepository=http://artifactory.pnet.ch/artifactory/swisspost
Snapshot Repository:
gradle uploadarchives -Prepository=http://artifactory.pnet.ch/artifactory/swisspost -PuploadRepository=http://artifactory.pnet.ch/artifactory/libs-release-local -PsnapshotRepository=http://artifactory.pnet.ch/artifactory/libs-snapshot-local
The DelegateName recognition pattern has to be improved to allow the following calls:
/gateleen/server/delegate/v1/delegates//definition
/gateleen/server/delegate/v1/delegates//execution
/gateleen/server/delegate/v1/delegates//execution/
/gateleen/server/delegate/v1/delegates//execution/xxx
/gateleen/server/delegate/v1/delegates//
/gateleen/server/delegate/v1/delegates/
The queued requests are not logged with the x-queue Header. This makes it impossible to identify them in the log files.
Find a way to log the request before it is queued and the x-queue header gets removed (QueuingHandler, line 57)
Solution
There are integration tests in gateleen which are not testing gateleen functionality, but redisques functionality. These tests should be moved to https://github.com/swisspush/vertx-redisques:
Currently, delegates don’t check if the incoming request URL matches the given pattern (they only check the grouping).
With INFO level, we should only see one log per request.
We currently have handlers logging too much:
2016-05-31 14:33:06,736 v000h8.pnet.ch_development test eagle WARN Forwarder - %cSaW Translated status 202 to 200
2016-05-31 14:33:06,736 v000h8.pnet.ch_development test eagle INFO LoggingHandler - %cSaW About to log to destination default
2016-05-31 14:33:06,739 v000h8.pnet.ch_development test eagle INFO RequestPropertyFilter - %cSaW Request to ...
There are some integration tests in gateleen-test which are failing sometimes. We should either stabilize or delete them:
Its not possible to use the EventBusHandler to create an eventbus bridge with sockjs.
The request to */sock/info is not handled by the EventBusHandler.
When a gateleen instance forwards to another one, the original x-server-timestamp header is used to calculate the expiration time in QueueProcessor. In case of delay or unsynchronized clocks, the expected TTL is not respected.
Proposal: add another header that would only be used internally by the queue handler. It will be set at enqueue time and removed when the request is played: x-queue-timestamp
This header would never be forwarded.
So we decouple the x-server-timestamp header and queue behaviour.
In the moment the integrationtests are in its own subproject. This is not the best solution, the integrationtests should be in the subproject where they belong too.
http://www.petrikainulainen.net/programming/gradle/getting-started-with-gradle-integration-testing-with-the-testsets-plugin/
The request logging feature seems to have a bug when a special combination of filter values (url, method, destination) are configured in the logging resource.
The following configuration defines a filter for an url and the destination where to log the requests. This configuration works without any problems:
{
"headers": [],
"payload": {
"destinations": [
{
"name": "requestLog",
"type": "file",
"file": "requestsTest.log"
}
],
"filters": [
{
"url": "/playground/server/tests/exp/.*",
"destination": "requestLog"
}
]
}
}
The following configuration also works without problems. This configuration again defines an url and the method (PUT) to log. The destination is configured with the system property org.swisspush.logging.dir:
{
"headers": [],
"payload": {
"destinations": [
{
"name": "requestLog",
"type": "file",
"file": "requestsTest.log"
}
],
"filters": [
{
"url": "/playground/server/tests/exp/.*",
"method": "PUT"
}
]
}
}
When using all three filter values (url, method and destination), the requests are not written to the logfile and the following error occurs:
log4j:ERROR Attempted to append to closed appender named [requestLog].
An example of such a logging resource would be:
{
"headers": [],
"payload": {
"destinations": [
{
"name": "requestLog",
"type": "file",
"file": "requestsTest.log"
}
],
"filters": [
{
"url": "/playground/server/tests/exp/.*",
"method": "PUT",
"destination": "requestLog"
}
]
}
}
According to the spec (https://tools.ietf.org/html/rfc3986#page-13), those chars are allowed in url path sements:
pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
/ "*" / "+" / "," / ";" / "="
path-abempty = *( "/" segment )
segment = *pchar
Reproduction:
Open Postman and PUT those resources to the gateleen storage (body does not matter) - note: only the last segment of the URL does matter:
http://nemotest.pnet.ch:7012/nemo/server/tests/collection/1_hello-$&()*+,;=
http://nemotest.pnet.ch:7012/nemo/server/tests/collection/2_hello-$&()*+,;=
http://nemotest.pnet.ch:7012/nemo/server/tests/collection/3_hello-$&()*+,;=
http://nemotest.pnet.ch:7012/nemo/server/tests/collection/1_hello-:$&()*+,;=
http://nemotest.pnet.ch:7012/nemo/server/tests/collection/4_hello-:@$&()*+,;=
Those path segments do not contain characters that need to be percent encoded (according to the spec).
Now do a GET using postman:
http://nemotest.pnet.ch:7012/nemo/server/tests/collection/
Result
{
"collection": [
"1_hello-$&()*+,;=",
"1_hello-§$&()*+,;=",
"2_hello-$&()*+,;=",
"3_hello-$&()*+,;=",
"4_hello-§@$&()*+,;="
]
}
Note those two results - the character §
- originally this was :
:
1_hello-§$&()*+,;=
4_hello-§@$&()*+,;=
GETing the resources (GET works with the URL used in PUT but DOES NOT work using the URL returned by the collection listing):
http://nemotest.pnet.ch:7012/nemo/server/tests/collection/4_hello-§@$&()*+,;=
http://nemotest.pnet.ch:7012/nemo/server/tests/collection/1_hello-§$&()*+,;=
http://nemotest.pnet.ch:7012/nemo/server/tests/collection/4_hello-:@$&()*+,;=
http://nemotest.pnet.ch:7012/nemo/server/tests/collection/1_hello-:$&()*+,;=
The problem can also be reproduced using the GUI:
We need a new Handler (MergeHandler) which allows us to perform a request over more than one route. To implement this, we first have to implement the issue #96.
The MergeHandler has to be addressed by a header: x-merge-collections
The value of the header is the base collection where the routes are created / hosted in.
"/gateleen/data/(.*)" : {
"path": "data/$1",
"staticHeaders": {
"x-merge-collections": "/gateleen/masterdata/parent/"
}
}
The simplest way is to read from all sources.
Example:
collection1: 123
collection2: 122
Execution:
// targeted request
GET /gateleen/data/collection/122
MergeHandler
GET /gateleen/masterdata/parent/collection1/data/collection/123 => 404
GET /gateleen/masterdata/parent/collection2/data/collection/122 => 200
// request on a collection
GET /gateleen/data/collection
MergeHandler
GET /gateleen/masterdata/parent/collection1/data/collection => 200
GET /gateleen/masterdata/parent/collection2/data/collection => 200
=>
{ "collection": [ "122", "123" ] }
404 is only returned if nothing is found in any of the given routes / sources.
The implementation has to be performed in a new module ‘gateleen-merge’.
If backends are gone all of the requests (from the queues as well as synchronous ones from the clients) might hang and run into a timeout. This can use up sockets pretty fast.
By introducing some kind of circuit breaker, the opening of connections to backends which timeout too often could be stopped and immediately return an error to the client for some time. This could prevent instances from "going down" by using up all its available sockets.
Check https://github.com/Netflix/Hystrix/wiki/Dashboard (Hystrix: Circuit Breaker from Netflix)
It seems that the hook expiration time is wrongly set. I observed one hour shift, probably because of missing timezone information in the stored timestamp.
Currently the schedulers are not executed at startup. If a scheduler only runs after a long period and the system is restarted before this period is over it never gets executed.
To ensure that a job is running at least once it should be possible to create a scheduler which is executed at startup.
Therefore I propose to introduce a new property.
Proposal:
"executeOnStartup" : true
Please comment.
For documentation and monitoring purposes, it would be useful to be able to extract information about the requests and their matching routing rule.
The request could be identfied by a configurable (system property) request header name. To identify the rule, an additional (unique) property called name could be added to each routing rule entry.
Add mime type matching to ZipExtractHandler.
eg. https://github.com/swisspush/vertx-rest-storage/blob/master/src/main/resources/mime-types.properties
In order to be able to implement the delegate system, hooks must be able to fire after the original request is performed (default is before). We should add a new attribute to the hook:
"type": "after"
Possible values: "before", "after". Defaults to "before".
Due to a missing JsonField the Delegates could not be found in the storage.
Since the introduction of the gradle-release plugin, the build on drone.io does not work anymore. See this build for example.
The error is:
Could not find net.researchgate:gradle-release:2.4.0
The dependency is listed as classpath dependency, so Drone.io cannot find it.
Please fix
Schedulers user cron expressions and are thus aligned with the clock. That means that all Gateleen servers with the same configuration will performs scheduled requests at the same time.
For requests going to a central communication server, this will introduce unbearable network load peaks.
We should add a randomness configuration value. This defines the maximal offset in seconds.
For example:
randomOffset: 300
means that the actual time can be shifted up to 5 minutes after the planned time.
Summary:
• We add a new configuration property to define the maximal offset in seconds.
• During the load of the scheduler configuration we calculate an offset for each scheduler, which stays the same for the specific schedulers, till the next reload of the scheduler configuration. This way the interval between executions of the schedulers remain always the same.
Some unit tests and the test module have been disabled for the build to work.
3061f8e
With the introduction of json schemas, the validation of the routing rules resource could be improved. However, not all restrictions are covered with the schema.
The following routing rule entry will result in a hanging request:
{
"/playground/": {
"description": "Home Page",
"storage": "main"
},
"/(.*)": {
"description": "Resource Storage",
"path": "/$1",
"storage": "main"
}
}
The problem with the entry above is the missing path property in the first rule. When a storage property is defined, the path property must be defined as well.
This restriction is not covered by the schema and has to be made manually. Looking at the logfile, the error has been logged successfully but the request was not responded.
java.lang.IllegalArgumentException: For storage routing, 'path' must be specified.
at org.swisspush.gateleen.routing.RuleFactory.setStorage(RuleFactory.java:113)
at org.swisspush.gateleen.routing.RuleFactory.createRules(RuleFactory.java:100)
at org.swisspush.gateleen.routing.RuleFactory.parseRules(RuleFactory.java:41)
at org.swisspush.gateleen.routing.Router.lambda$route$18(Router.java:147)
at org.swisspush.gateleen.routing.Router$$Lambda$158/1153343910.handle(Unknown Source)
This request should have been responded with a http status code 400.
There are integration tests in gateleen which are not testing gateleen functionality, but rest-storage functionality. These tests should be moved to https://github.com/swisspush/vertx-rest-storage:
Currently it’s not possible to list routes in a collection.
PUT /collection/resource1
PUT /collection/resource2/_hook/route
GET /collection/resource2 => HookHandler -> Forwarder
GET /collection => Resource storage => { "collection": [ "resource1" ] }
In order to be able to list routes in a collection, we have to adapt the HookHandler. This way a request to a parent collection will check, if routes exist and if so, list them properly. Therefor the HookHandler takes control of creating the listing, if a route - handled by the HookHandler – is present (not the Router), otherwise the Router takes control.
PUT /collection/resource1
PUT /collection/resource2/_hook/route
GET /collection/resource2 => HookHandler -> Forwarder
GET /collection => HookHandler -> Resource Storage + Route hook list => { "collection": [ "resource1", "resource2" ] }
Extend the configuration of Hooks for Routes with two new attributes ‘listable’ and ‘collection’.
listable => if true routes will be shown, otherwise not (current behavior)
collection => because it’s not visible by an URL, if it points to a collection or a resource, we need to specify if the rout is a collection (default) or a resource. This is necessary if the listable feature is used with for example the expand feature.
{ "destination": .., // as usual
"listable": false, // new, allows to override the default
"collection": false } // new, indicates if route points to collection or resource.
We also need a new constructor, which allows to set the default for the listable feature:
new HookHandler(...., true) // enable / disable listable as default
In the Queue Circuit Breaker we use periodic tasks (vertx.setPeriodic(...)) to execute tasks like unlocking sample queues and switching open circuits to state halfOpen.
These periodic tasks are schedulded by the configuration resource and then executed in every verticle instance.
This means unlocking a locked queue will result in multiple (corresponding the count of verticles) delete lock requests.
This behaviour should be changed, so every task execution is made by a single instance only. Use the SETNX command from redis to achieve this.
Another solution could be cluster wide locks. See http://vertx.io/docs/vertx-core/java/#_cluster_wide_locks
With the current implemenation we can define static headers for routing rules like this:
{
"/playground/tests/(.*)": {
"path": "/playground/server/tests/$1",
"storage": "main",
"staticHeaders": {
"X-Expire-After": "3600"
}
}
}
Defining a X-Expire-After static header like in the example above will add this header to the request. However, an already added X-Expire-After header will be replaced.
It would be useful to have a feature which allows to add a static headers only when they are not already present.
This could be accomplished by adding a forced flag. This flag would result in the following behaviour:
Force flag | Behaviour |
---|---|
true | Replaces (overrides) already defined header values with the staticHeader value |
false | Does nothing when header value already exists, adds defined header value otherwise |
not set | Replaces (overrides) already defined header values with the staticHeader value. This is used for backward compatibility, since headers are replaced in current implementation |
A possible staticHeader configuration could look like this:
{
"/playground/tests/(.*)": {
"path": "/playground/server/tests/$1",
"storage": "main",
"staticHeaders": {
"X-Expire-After": { "value": "3600", "force": true }
}
}
}
Extend the ZipHandler of the ExpansionHandler to use a zip method without compression. This could be helpful, if there is a need to pack some zips together in one zip, to save PUT / GET requests.
Currently:
zip=true
New:
zip=ture (compressed)
zip=store (uncompressed)
The LoggingHandler class can be used to log the payload of requests to a log file. We have found situations where requests which should be logged (according to the logging configuration) have not been found in the log file.
In this case, the configuration should be correct since other requests of the same pattern could be successfully logged.
I would suggest to investigate in the logging code and make the improvements/reviews listed below:
As additional information it would be helpful to add the timestamp to the requests log entries!
In QueueHandler and HookHandler, the QueueClient is instanciated with new.
I propose to make it overridable in order to allow custom implementation.
For example, on single-instance gateleen servers where we don't need persistence we can enqueue in memory, without redisques.
If no listeners with the trigger type before are present, the after hooks won’t be fired. Even the original request won’t be fired.
In order to be able to optimize the transport from a source via hook to the target, we have to be able to define how long a request remains maximally in the queue. The TTL for the request in the queue shouldn’t be the TTL of the resource (that’s currently the default behavior).
To be able to separate the TTL of a resource and the TTL of a request in the queue, we need an additional header x-queue-expire-after, which enables us to override the default setting of the TTL in the queue.
If the x-queue-expire-after header is set, this header will be used instead of the header x-expire-after to determine if a request in the queue expires or not. If the header is not set, the current behavior will not change.
The Scheduler doesn’t currently set a ‘X-Server-Timestamp’ header when it enqueues a request.
Because of this the expiration of schedulers queues is broken.
Add the "X-Server-Timestamp" header to each request of a schedulers, when it is enqueued.
Implement the delegate feature. With this mechanism we are able to replace a request by another.
For example, we could make a GET that will be replaced by another one (or even a POST). This allows for creating cheap adapters.
Cookbook for the delegate:
Name a delegate:
/gateleen/server/delegate/v1/delegates/<delegate>/
Create a definition:
/gateleen/server/delegate/v1/delegates/<delegate>/definition
DelegateHandler automatically checks if the Pattern
/gateleen/server/delegate/v1/delegates/<delegate>/execution
is called.
Example:
// create a delegate
PUT /gateleen/server/delegate/v1/delegates/user-zip-copy/definition
{
"methods": [ "PUT", "DELETE" ],
"pattern": ".*/([^/]+.*)",
"requests": [
{
"method": "POST",
"url": "/gateleen/server/v1/copy",
"payload": {
"source": "/gateleen/$1?expand=100&zip=true",
"destination": "/gateleen/zips/users/$1.zip"
}
}
]
}
}
// create a hook triggering the delegate
PUT /gateleen/users/_hooks/listeners/user-zip-copy
{
"destination": "/gateleen/server/delegate/v1/delegates/user-zip-copy/execution"
}
To reproduce follow these steps:
{
"resources": [
{
"url": "/resources/resourceToBeValidated",
"method": "PUT"
}
]
}
Result: The PUT request for the resourceToBeValidated resource is never responded!
Reuse the already existing metricName property for the request per rule monitoring
Work through this resource and assure that we doing it right
https://www.mnot.net/blog/2011/07/11/what_proxies_must_do
We should be able to directly address a resource inside a zip file. Therefor we need to create an ZipExtractHandler.
GET /gateleen/zips/111111.zip/this/is/my/resource -> ZipExtractHandler
The current implementation (circuitbreaker_update.lua) calculates the failRatio of an endpoint based on the amount of recorded statistic values (failed and succeeded requests) and the ratio between the count of those statistic values.
sampleCountThresholdReached
Whether or not the amount of recorded statistic values is reached, is decided by counting the entries in the success/fail sets using the redis command ZCARD. The ZCARD command does simply return the amount of entries in a sorted set not respecting the score.
calculateFailurePercentage
The failure percentage is also calculated based on the amount of entries in the success/fail sets. However, this time, the redis command ZCOUNT is used. The ZCOUNT command has to be executed with additional score parameters. In this case, the score parameter relates to the timestamp when the entry was added to the set. So also entries not older than Now - entriesMaxAgeMS are respected for the fail ratio calculation.
Problem
Since the sampleCountThresholdReached does not respect the age of the entries, all entries are counted. This leads to a sampleCountThresholdReached = TRUE once the minQueueSampleCount _ treshold has reached.
From now on, sampleCountThresholdReached is always true (until the endpoint has opened and closed again resulting in a reset of the endpoint statistics and failRatio) and therefore only the calculateFailurePercentage value is respected.
The problem here is when in the configured entriesMaxAgeMS range a single failing request is made, the failRatio calucation will look like this:
Count successful: 0
Count failure: 1
failRatio: (1/1)*100 => 100%
The endpoint will be opened based on just one failing request.
Solution
Like the calculateFailurePercentage value, the sampleCountThresholdReached value has to be calculated using the redis command ZCOUNT which respects the age of the entries.
In the scenario mentioned above, this will lead to a sampleCountThresholdReached = FALSE, since only one entry is respected.
If a delegate request without a payload is created and executed, a NullpointerException arises. This due to the fact, that it’s not checked, if a body is null or not before creating a HttpClientRequest.
In the QueueBrowser class is a mapping for GET requests to load a single QueueItem by index.
GET /playground/queues/my_queue/{index}
This mapping is implemented as follows:
// Get item
router.getWithRegex(prefix + "/queues/([^/]+)/[0-9]+").handler(ctx -> {
final String queue = lastPart(ctx.request().path().substring(0, ctx.request().path().length() - 2), "/");
final int index = Integer.parseInt(lastPart(ctx.request().path(), "/"));
eb.send(redisquesAddress, buildGetQueueItemOperation(queue, index), new Handler<AsyncResult<Message<JsonObject>>>() {
@Override
public void handle(AsyncResult<Message<JsonObject>> reply) {
JsonObject replyBody = reply.result().body();
if (OK.equals(replyBody.getString(STATUS))) {
ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON);
ctx.response().end(decode(reply.result().body().getString(VALUE)));
} else {
ctx.response().setStatusCode(StatusCode.NOT_FOUND.getStatusCode());
ctx.response().setStatusMessage(StatusCode.NOT_FOUND.getStatusMessage());
ctx.response().end("Not Found");
}
}
});
});
The following line is wrong, since it only works for indices from 0 to 99
final String queue = lastPart(ctx.request().path().substring(0, ctx.request().path().length() - 2), "/");
Making a request like this
GET /playground/queues/my_queue/500
would result in a queuename with value 5 instead of my_queue.
In the Router class an ellipsis of doneHandlers can be passed.
Handler<Void>... doneHandlers
The purpose of these doneHandlers is to get notified when the router is completely initialized.
Every time the routing rules are changed, the method
void updateRouting(List<Rule> rules)
is called. This method includes the notification of the doneHandlers with the following loop
for (Handler<Void> doneHandler : doneHandlers) {
doneHandler.handle(null);
}
This behaviour is currently used in the HookHandler class. There, with every notification of the doneHandler the init method is called
public void init() {
registerListenerRegistrationHandler();
registerRouteRegistrationHandler();
loadStoredListeners();
loadStoredRoutes();
registerCleanupHandler();
}
leading to new consumers. The creation of new consumers in this case should be avoided.
To await the complete initialization of the Router class, a RouterFactory could be implemented which builds the Router and returns a Future holding the initialized Router. This could look something like this:
public class RouterProvider {
public Future<Router> buildRouter(Object param1, Object param2, Object param3) {
Future<Router> future = Future.future();
// instantiate the router and call future.complete() when ready
return future;
}
}
The doneHandlers ellipsis parameter could then be removed from the constructors.
Please comment.
In the gateleen-test module the test testExtractZipContentFound from class ZipExtractTest does fail every time when executed on drone.io but succeeds locally.
The test always fails on the following assertion
// compare if the extracted file is identical to the original file
context.assertTrue(Files.equal(originalFile, responseFile));
Please fix this test
We have to adapt the HookHandler to be able to define staticHeaders in a hook.
"hook": { "methods": [ "PUT" ], "destination": "/test", "expireAfter": 86400, "staticHeaders" : { "x-queue-expire-after" : 20 } }
Static headers overrides always currently set headers (as well as default headers).
Let's say you have the following structure of resources and want to validate the res_1, res_2 and res_3 resource.
To validate these resources, you would have to deploy 3 different schemas and configure them in the validation resource like this:
{
"resources": [
{
"url": "/playground/server/tests/resources/res_1",
"method": "PUT"
},
{
"url": "/playground/server/tests/resources/res_2",
"method": "PUT"
},
{
"url": "/playground/server/tests/resources/res_3",
"method": "PUT"
}
]
}
So when res_1, res_2 and res_3 are instances of the same resource and therefore can be validated by the same schema, it would be useful to define one schema only.
It would be useful when the configuration could look something like this:
{
"resources": [
{
"url": "/playground/server/tests/resources/.*",
"method": "PUT"
}
]
}
This would mean that all resources under /playground/server/tests/resources/ will be validated. It has to be defined how the corresponding schema for this resources can be selected (maybe by defining the name of the schema as additional property in the validation resource).
The logging for the Queue Circuit Breaker should be enhanced. It has to be checked where the logging could be improved.
One place to enhance the logging is:
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.