Giter Club home page Giter Club logo

b3scale's Introduction

B3scale

Test Go Report Card

An efficient multi tenant load balancer for BigBlueButton.

About

b3scale is a load balancer designed to be used in place of scalelite. Work was started in 2020 to provide multiple features not possible before:

  • API driven: REST API allows integrating b3scale straight to your CRM and/or user portal
  • Observable: Prometheus endpoint for all essential operational information
  • Multi tenancy: b3scale introduces the concept of frontends
    • custom client secret
    • optional custom presentation
  • Efficient: 5 figure users with a single instance
  • High availability and easy scale out: Just add more b3scale servers and use with your existing HA solution
  • Flexible backend handling:
    • Map frontends to BBB nodes (backends)
    • Retire backends for updates
    • Powerful tagging system allows for friendly user testing, experiments and assignments depending on expected customer load
  • True load-awareness: using reports from the b3scalenoded agent, b3scale can schedule meetings more efficiently
  • Easy deployment: b3scale is written in Go: no dependencies, just deploy a single binary

Basic principle

To discuss the principal design of b3scale, consider the following schematic:

b3scale architecture

b3scale services different frontends. Those can be standard apps such as Greenlight, Nextcloud or Moodle, but can also really be anything that implements the BigBlueButton API, even custom web apps.

A frontend can initiate a new meeting via b3scale, which will assign it to a backend node and will keep track of the assignment. Users joining will thus be assigned to the correct backend.

Using tags you can assign specific roles to one or more backend nodes: you can assign a customer to a specific set of nodes, effectively forming a dedicated cluster. It is also possible to steer friendly users towards nodes that contain experimental features.

You can take a backend offline by disabling it. This will not affect currently running meetings. It will only remove the node from consideration for new meetings. This way, backend nodes can be drained e.g. in preparation for scheduled maintenance.

For backends nodes, b3scale provides b3scalenoded, an agent for backend nodes that monitors certain parameters straight from redis and reports them to b3scale in an inexpensive, resource conserving fashion.

DEPRECATION NOTICE: The b3scalenoded will be deprecated in favour of the b3scaleagent, which does the same thing, but uses the HTTP API.

Please note, that the agents need unique access tokens for each backend.

A new access token can be crated using b3scalectl auth authorize_node_agent.

Terminology

  • Frontend: A BigBlueButton frontend such as Greenlight
  • Backend: A BigBlueButton server to distribute meetings on
  • Middleware: Different middleware implementations provide different aspects of b3scale's core functionality such as tagging

API documentation

Please find the API documentation in the REST API file.

Configuration

b3scale daemons are configured through environment variables and do not use a config file. Example environment files for use with Docker, Kubernetes or systemd with all eligable settings can be found here:

Find more documentation below.

Environment variables

  • B3SCALE_LISTEN_HTTP Accept http connections here. Default: 127.0.0.1:42353

  • B3SCALE_DB_URL is the connect string passed to the database connection. Default is postgres://postgres:postgres@localhost:5432/b3scale

    You can use either the DSN format or an URL format:

    Example DSN

    user=jack password=secret host=pg.example.com port=5432 dbname=mydb sslmode=verify-ca

    Example URL

    postgres://jack:[email protected]:5432/mydb?sslmode=verify-ca

    (b3scaled and b3scalenoded only)

  • B3SCALE_DB_POOL_SIZE the number of maximum parallel connections we will allocate. Please note that one connection per request will be blocked and returned to the pool afterwards.

    Default: 128

  • B3SCALE_LOG_LEVEL set the log level. Possible values are:

     panic  5
     fatal  4
     error  3
     warn   2
     info   1
     debug  0
     trace -1
    

    You can use either the numeric or integer value

  • B3SCALE_LOG_FORMAT choose between plain or structured logging. The default is structured and will emit JSON on stderr.

Same applies for the b3scalenoded, however only B3SCALE_DB_URL is required.

The b3scalenoded and b3scaleagent read from the same configuration as BigBlueButton, the environment variable for the file is:

  • BBB_CONFIG, which defaults to: /etc/bigbluebutton/bbb-web.properties You probably want to keep this value.

This file must be readable for the b3scalenoded.

  • B3SCALE_ACCESS_TOKEN stores the authorized access token for a node.

You can authorize a new agent using b3scalectl:

b3scalectl auth authorize_node_agent

Unless the B3SCALE_API_JWT_SECRET environment variable is set, you will be prompted to enter the API secret.

As an alternative the secret can be provided through the --secret flag when authorizing a new agent.

A custom agent identifier can be provided through the --ref option. If none is provided, it will be generated.

A full example would be

b3scalectl auth authorize_node_agent --ref backend23 --secret my-api-secret

Please note, that the ref identifier must be unique, as only one backend can be associated with an agent.

  • B3SCALE_API_URL must be provided for the b3scaleagent to find the API. Only the host part is required: e.g. https://b3scale.example/

The load factor of the backend can be set through:

  • B3SCALE_LOAD_FACTOR (default 1.0)

  • B3SCALE_API_JWT_SECRET if not empty, the API will be enabled and accessible through /api/v1/... with a JWT bearer token. You can set the jwt claim scope to b3scale:admin to create an admin token. You can generate an access token using b3scalectl:

    b3scalectl auth create_access_token --sub node42 --scopes b3scale,b3scale:admin,b3scale:node
    

    You are then prompted to paste the B3SCALE_API_JWT_SECRET.

    You can pass the secret through the --secret longopt - however this is discouraged because it might end up in the shell history. Be careful.

    In case your b3scalectl responds with

     3:43PM FTL this is fatal error="message: invalid or expired jwt"
    

    remove the access token in ~/.config/b3scale/<host>.access_token

  • B3SCALE_RECORDINGS_PUBLISHED_PATH required if recordings are supported: This points to the shared path where published recordings are. Example: /ceph/recordings/published

  • B3SCALE_RECORDINGS_UNPUBLISHED_PATH recordings are moved here, when unpublished Please note that in both directories the subfolder for the format should be present. (e.g. /ceph/recordings/unpublished/presentation) Example: /ceph/recordings/unpublished

  • B3SCALE_RECORDINGS_PLAYBACK_HOST path to host with the player. For example: https://playback.mycluster.example.bbb/

Adding Backends

Using the node agent

Adding a backend using the node agent b3scalenoded / b3scaleagent can be as simple as starting it with the -register option for autoregistering the new node.

For the b3scaleagent the B3SCALE_ACCESS_TOKEN and B3SCALE_API_URL needs to be provided.

The node is identified through the BBB_CONFIG file from bbb-web.

Autoregistering is turned off by default.

After registering the node, you have to enable it.

The default admin_state of the node is init. To enable the node, set the admin state to ready.

$ b3scalectl enable backend https://bbbb01.example.net/bigbluebutton/api/

The host should match the one you see with

$ b3scalectl show backends

Disable Backends

You can exclude backends as targets for new meetings by running

$ b3scalectl disable backend https://bbbb01.example.net/bigbluebutton/api/

Deleting Backends

Backends can be removed through

$ b3scalectl rm backend https://bbbb01.example.net/bigbluebutton/api/

This will initiate a decomissioning process, where the backend will not longer be used for creating new sessions.

It will be permanently deleted after the last session was closed.

Middleware Configuration

The middlewares can be configured using b3scalectl or via API calls. A property value will be interpreted as JSON.

Configure tagged routing

b3scalectl set backend -j '{"tags": ["asdf", "foo", "bar"]}' https://backend23/
b3scalectl set frontend -j '{"required_tags": ["asdf"]}' frontend1

Unset a value with explicit null:

b3scalectl set frontend -j '{"required_tags": null}' frontend1

Configure a default presentation

b3scalectl set frontend -j '{"default_presentation": {"url": "https://..."}}' frontend1

Configure create parameter defaults and overrides

An override will replace the parameter of the request.

A default is added to the request parameters if not present. In case of disabledFeatures, the list coming from the request will be amended with the defaults. If no disabledFeatures are provided from the frontend, the defaults will be used.

All parameter values are strings and need to be encoded according to the specifications in https://docs.bigbluebutton.org/dev/api.html#create

Some examples:

Set a default logo (if not present) and force some disabled features. Addional disabled features from the frontend will be preserved.

b3scalectl set frontend -j '{"create_default_params": {"logo": "logoURL", "disabledFeatures": "chat,captions"}}' frontend1

Force disable recordings:

b3scalectl set frontend -j '{"create_override_params": {"allowStartStopRecording": "false", "autoStartRecording": "false"}}' frontend1

Set disabledFeatures, ignoring requested disabled features from the frontend:

b3scalectl set frontend -j '{"create_override_params": {"disabledFeatures": "captions"}}' frontend1

Setting create_default_params or create_override_params is always a replacement of the current value. If null is provided, the configuration key will be unset.

b3scalectl set frontend -j '{"create_override_params": null, "create_default_params": null}' frontend1

Monitoring

Metrics are exported in a prometheus compatible format under /metrics.

Bug reports and Contributions

If you discover a problem with b3scale or have a feature request, please open a bug report. Please check the existing issues before reporting new ones. Do not start work on new features without prior discussion. This helps us to coordinate development efforts. Once your feature is discussed, please file a merge request for the develop branch. Merge requests to mainhappen from develop only.

Disclaimer

This project uses BigBlueButton and is not endorsed or certified by BigBlueButton Inc. BigBlueButton and the BigBlueButton Logo are trademarks of BigBlueButton Inc.

b3scale's People

Contributors

annikahannig avatar danimo avatar dependabot[bot] avatar derpeter avatar henning-unicode avatar johannwagner avatar sebhoss avatar zuntrax avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

b3scale's Issues

Make join() parameters customizable per frontend

Useful for allowing theming where the userdata_ elements (e.g. for passing custom CSS) only be submitted during join(), not create(). This allows for features like theming to work per frontend.

Limiting number of attendees per frontend/tenant

We are thinking about implementing a limit on attendees per frontend/tenant.

Has this been discussed yet? Do you think it is feasible? Limiting the number of meetings would probably be a similar task.

From my understanding we would need to do at least:

  1. Add a new metric b3scale_frontend_attendees that is aggregated for all frontends
  2. Add a "limit_attendees" variable for the frontends (can frontends have additional information attached to them besides tags right now?)
  3. Add a middleware (?) that checks 2) against 1) and stops joining or creating a meeting if the limit is reached.

I am trying to add new frontend

Can you guide me about how greenlight will communicate to b3scale. in greenlight .env should i mention the b3scale api as

BIGBLUEBUTTON_ENDPOINT=https://meetha.domain.com
BIGBLUEBUTTON_SECRET=********************************

or it should be bbb-conf --secrets

Thanks for you assistance.

Bug: Meeting stats are destroyed by calling BBB create endpoint multiple times

Expected behaviour:

Meeting Info is correct and up to date all the life time of a meeting.

Actual behaviour:

Every time a user joins a meeting through greenlight a new create request is send to b3scale resulting in reseting all meeting state (like participantcount etc.).

Investigation:

The bug happens in pkg/cluster/backend.go 395:

	} else {
		// Update state, associate with backend and frontend
		meetingState.Meeting = createRes.Meeting
		meetingState.SyncedAt = time.Now().UTC()
		if err := meetingState.Save(ctx, tx); err != nil {
			return nil, err
		}
	}

createRes.Meeting needs to be prepopulated with current meeting state values before updating state with new meeting.

It is currently not clear to us, what this not restful hidden update on create functionality is actually used for.

x509: certificate signed by unknown authority

Hi There,
I have been trying to deploy b3scale in kubernetes, which I have done successfully. After deploying it and adding the new backend b3scale sees my backend URL as certificate signed by unknow authority. While the same time implementation in VM works fine with same backend.

See the logs output from api client.

b3scalectl --api https://****************** show backends
fb555a66-705f-4edb-a328-e3d33313b009
  Host:	 https://******************/bigbluebutton/api/
  Settings:	 {}
  NodeState:	 error	  AdminState:	 ready
  MC/AC/R:	 0/0/0.00
  LoadFactor:	 1
  Latency:	 0s
  LastError: Get "https://****************/bigbluebutton/api/getMeetings?checksum=a1d02b27f954c439c83d3844c634755084a0ba1fef184eb6932b1e7976a364a0": x509: certificate signed by unknown authority

What could be the issue may be.

Issue in adding backends with b3scalectl

The workflow of adding backends is not clearly documented. Using auto registration by the node agent works. But b3scalectl claims to be able to add backends as well. Also the docs claim, that registration by node agents is only one way.

Autoregistering is turned off by default.

Adding a backend on b3scalectl throws an unhandled exception:

$> b3scalectl  add backend --secret "secret" https://my-bbb-server.org/bigbluebutton/api/
using environment from: .env
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x78 pc=0x8ee25b]

goroutine 1 [running]:
main.(*Cli).setBackend(0xc000112670, 0xc00008bd00)
	/home/runner/work/b3scale/b3scale/cmd/b3scalectl/backends.go:94 +0x23b
github.com/urfave/cli/v2.(*Command).Run(0xc0000abd40, 0xc00008bb00)
	/home/runner/go/pkg/mod/github.com/urfave/cli/[email protected]/command.go:163 +0x5bb
github.com/urfave/cli/v2.(*App).RunAsSubcommand(0xc0000c71e0, 0xc00008b9c0)
	/home/runner/go/pkg/mod/github.com/urfave/cli/[email protected]/app.go:434 +0xc8a
github.com/urfave/cli/v2.(*Command).startApp(0xc0000abc20, 0xc00008b9c0)
	/home/runner/go/pkg/mod/github.com/urfave/cli/[email protected]/command.go:278 +0x713
github.com/urfave/cli/v2.(*Command).Run(0x933080?, 0xc000066920?)
	/home/runner/go/pkg/mod/github.com/urfave/cli/[email protected]/command.go:94 +0xba
github.com/urfave/cli/v2.(*App).RunContext(0xc0000c7040, {0xac56a8?, 0xc000022090}, {0xc00001e080, 0x8, 0x8})
	/home/runner/go/pkg/mod/github.com/urfave/cli/[email protected]/app.go:313 +0xb48
main.(*Cli).Run(...)
	/home/runner/work/b3scale/b3scale/cmd/b3scalectl/cli.go:516
main.main()
	/home/runner/work/b3scale/b3scale/cmd/b3scalectl/main.go:35 +0x166

Possibly the command is invoked wrong, but certainly the cli should not fail with segfault...

api/v1/recordings-import endpoint is broken

Sending any authorized post request to this endpoint on tag 1.0 / master ends up in an "internal server error".

Log message is:

connection missing in context

which is thrown in store/context.go

We found a one line fix and attach a PR with this issue.
Maybe a flaw in code refactoring?

messageKey for checksum error not bbb compliant

According to the BBB API docs (https://docs.bigbluebutton.org/development/api/) an invalid checksum should return the messageKey checksumError; also the returncode should be FAILED not ERROR

<response>
<returncode>FAILED</returncode>
<messageKey>checksumError</messageKey>
<message>Checksums do not match</message>
</response>

However b3scale returns messageKey b3scale_server_error:

<response>
<returncode>ERROR</returncode>
<message>invalid checksum</message>
<messageKey>b3scale_server_error</messageKey>
</response>

Improve Readme with installation instructions for newbies

First of all, thank you for this wonderful loadbalancer. I got a first test instance running and it looks quite promising.

During the installation however, I ran into some questions that could probably easily be avoided by extending the Readme with an "Installation" section that explains the most basic setup. Since I just went through that, I could put up a PR for that.

Just let me know if it is worthwhile to include a description of the scalenoded as well (which I think is currently a requirement to get it up and running), or if this will change too soon to be bothered with in favor of the b3scaleagent.

Use most common default for BBB_CONFIG

BBB_CONFIG is set by default to /usr/share/bbb-web/WEB-INF/classes/bigbluebutton.properties and the readme mentions that in most cases this needs to be changed to /etc/bigbluebutton/bbb-web.properties

It would be good to have it set to /etc/bigbluebutton/bbb-web.properties by default, as bbb-conf is setting the hostname in the overwrite file.

This should be agjusted in the following files:

b3scale-operator documentaion

Hi,
I was wondering if we do have documentation related to b3scale operator. I have tried to deploy it in the beginning it was failing we had to update the operator.yaml to mount the config as below

volumeMounts: - mountPath: "/b3scale-operator-config.yaml" name: b3scale-operator-config subPath: b3scale-operator-config.yaml

else container was unable to find the config.yaml.

Moving forward how to use it with b3scalectl. How can we add/remove backends and fronted with it.

API: Unsetting Frontend settings is not possible

Problem

We are currently implementing a b3scale-operator. While testing the functionality, we found, that it is not possible to unset values within the settings object. They stay the same.

Reproduction

b3scalectl set  frontend -j  '{"required_tags": ["foobar"]}' b3scale-operator-testeroni
b3scalectl set  frontend -j  '{"required_tags": []}' b3scale-operator-testeroni

Current Behaviour

required_tags stays foobar.

Expected Behaviour

required_tags should be set to [].

Only one registered person can join through b3scale with Greenlight 3

Bug description

When using Greenlight 3 as a frontend to b3scale, only the first join of a (Greenlight) registered user succeeds. All subsequent joins fail. Joining as a guest does not fail

Prelim. Analysis

Prior to joining an existing meeting, Greenlight calls create to ensure the meeting exists. This should just work, and does with other frontends. According to the BBB API reference:

The create call is idempotent: you can call it multiple times with the same parameters without side effects. This simplifies the logic for joining a user into a session as your application can always call create before returning the join URL to the user. This way, regardless of the order in which users join, the meeting will always exist when the user tries to join (the first create call actually creates the meeting; subsequent calls to create simply return SUCCESS).

However, in case of Greenlight 3's createcall, while b3scale passes it to the BBB backend, the backend returns an unexpected error. Accordingly, b3scale returns a gateway error to the frontend.

How to manage the load manually

Hi There,

Thanks for the documentation other day was very helpful. I just set up a testing Environment. I have couple of questions to understand the feature set of this awesome loadbalancer.

  • How can we manually mention the load i.e to prioritize and schedule meetings on one of the backend with high specs and mention the load on one of the other backend which is less specs (or the meeting is scheduled always in roud robin way)
  • How can we send one [email protected] to a specific backend & [email protected] to another backend.
    Thanks again for the wonderful documentation.

Regards,
Naeem

Make `create()` parameters customizable per tenant

We already support setting a presentation per tenant. We can improve on that by writing a middleware that allows setting even more overrides passed to the create() call. Here are some scenarios:

  • As a tenant, I want to be able to choose brand colors, logo, welcome text, etc so I can be* tter adjust the look to match my corporate design or a particular use case
  • As a tenant, I want to customize default layout depending on my use case (camera-focussed layout by default, etc)
  • As a tenant, I want to globally disable the learning dashboard due to legal requirements
  • As a service provider, I want to enable/disable features such as recording for tenants to accommodate for legal requirements

Extended Reading: https://docs.bigbluebutton.org/dev/api.html#create, esp. disabledFeatures.

(Migrated from https://gitlab.com/infra.run/public/b3scale/-/issues/12)

getMeetingInfo-call: messageKey of error does not match BBB-API

I noticed that the messageKey of the error message you can get by calling getMeetingInfo with a non-existent meetingID does not return the same as the official BBB-API.

This breaks applications which rely on the content of messageKey - e.g. scalelite.

I guess by changing the key here it should work again.

Calling a BBB-server

curl "https://some.bbb.server.local/bigbluebutton/api/getMeetingInfo?meetingID=this-does-not-exist&checksum=<checksum>"
<response>
<returncode>FAILED</returncode>
<messageKey>notFound</messageKey>
<message>A meeting with that ID does not exist</message>
</response>

Calling a b3scale-API

curl "https://some.b3scale.server.local/tenant/api/getMeetingInfo?meetingID=this-does-not-exist&checksum=<checksum>"
<response>
<returncode>FAILED</returncode>
<message>The meeting is not known to us.</message>
<messageKey>invalidMeetingIdentifier</messageKey>
</response>

Switch b3scalenoded to REST API

(Migrated from mhttps://gitlab.com/infra.run/public/b3scale/-/issues/13)

Currently, b3scalenoded directly connects to PostgreSQL. This should be switched to using the REST API. Motivations:

  • Allow running b3scale in Kubernetes without a dedicated Postgres-Ingress.
  • Privilege separation: less risk in case a single node gets compromised down to root access.

unable to add new backends

I have newly deployed b3scale in kuberenetes.

`kubectl -n b3scale get deploy,pods,ingress,svc
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/b3scale 1/1 1 1 27m

NAME READY STATUS RESTARTS AGE
pod/b3scale-6b56d5bf6b-2n5b9 1/1 Running 0 26m

NAME CLASS HOSTS ADDRESS PORTS AGE
ingress.networking.k8s.io/b3scale nginx b3scale.demo.ing 172.25.25.139,172.25.25.185 80 24m

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/b3scale ClusterIP 10.43.168.228 42352/TCP 25m`

I can check the version while making an api call to ingress

b3scalectl --api http://b3scale.demo.ing version b3scalectl v.1.0.2 15e101d45873bdd499e279a30ccc5fafdf9af14c API status: &{1.0.3-rc5 96e6db7b0f51db4c1aba3a74e3e85f59357d272c v1 b3scalectl true 0xc000124240}

Logs from the pod

{"level":"info","remote_ip":"172.25.25.1","host":"b3scale.demo.ing","method":"GET","uri":"/api/v1","user_agent":"Go-http-client/1.1","status":200,"referer":"","latency":894.972014,"latency_human":"894.972014ms","bytes_in":0,"bytes_out":332,"time":"2023-11-05T07:52:09Z"} {"level":"info","remote_ip":"172.25.25.1","host":"b3scale.demo.ing","method":"GET","uri":"/api/v1","user_agent":"Go-http-client/1.1","status":200,"referer":"","latency":273.349317,"latency_human":"273.349317ms","bytes_in":0,"bytes_out":332,"time":"2023-11-05T07:52:09Z"}
While trying to add the backend, I receive the below response

โฏ b3scalectl --api http://b3scale.demo.ing add backend https://oldmeet.demo.ing --secret SnbF55LU7AJTqzkLjqMnrp8T6w1ssyN0yZ0VAeXX0 no changes
Logs from the pod while trying to add the backned

{"level":"info","remote_ip":"172.25.25.1","host":"b3scale.demo.ing","method":"GET","uri":"/api/v1","user_agent":"Go-http-client/1.1","status":200,"referer":"","latency":706.862792,"latency_human":"706.862792ms","bytes_in":0,"bytes_out":332,"time":"2023-11-05T07:55:09Z"} {"level":"info","remote_ip":"172.25.25.1","host":"b3scale.demo.ing","method":"GET","uri":"/api/v1/backends?host=https%3A%2F%2Foldmeet.demo.ing%2F","user_agent":"Go-http-client/1.1","status":200,"referer":"","latency":331.535015,"latency_human":"331.535015ms","bytes_in":0,"bytes_out":3,"time":"2023-11-05T07:55:10Z"}

Is there a docker image for b3scale.

Hello,

As I am trying to run it as a container in kubernetes, is there any quick start and docker image to deploy in kubernetes.

Regards,
Naeem

Import old recordings

I have some old recordings that were created on a BBB server before we used b3scale. I now use b3scale as a load balancer in front several Greenlight 2 frontends. I want to make these old recodings available for a Greenlight tenant at /admins/recordings.

I'm not sure how to do this. What I tried:

  • Check that a entry in the table frontends for that specific Greenlight frontend exists
  • Determine the meeting_id from the metadata.xml of the recording
  • Manually create a new entry to the table frontend_meetings containing the frontend_id and the meeting_id of the recording
  • Run the import via API call (api/v1/recordings-import)
  • After that check that an entry in the table recordings was successfully created

Nevertheless the specific recoding isn't available at the Greenlight frontend but all other recordings that were created using b3scale as load balancer are available.

What am I missing? Thanks for your help.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.