Giter Club home page Giter Club logo

singyeong's Introduction

신경

Build status codecov Docker Hub Dependabot Status

신경

/ɕʰinɡjʌ̹ŋ/ • sin-gyeong (try it with IPA Reader)

  1. nerve

Nerve

/nərv/ • noun

  1. (in the body) a whitish fiber or bundle of fibers that transmits impulses of sensation to the brain or spinal cord, and impulses from these to the muscles and organs.

"the optic nerve"

신경 is the nerve-center of your microservices, providing a message bus + message queue with powerful routing, automatic load-balancing + failover, powerful HTTP request proxying + routing, and more. 신경 aims to be simple to get started with while still providing the features for a variety of use-cases.

신경 is a part of the amyware Discord server.

If you like what I make, consider supporting me on Patreon:

Clients:

Language Author Link Maintained?
Elixir @queer https://github.com/queer/singyeong-client-elixir yes
.NET @FiniteReality https://github.com/finitereality/singyeong.net yes
Python @PanKlipcio https://github.com/StartITBot/singyeong.py yes
Typescript @cyyynthia https://github.com/borkenware/singyeongjs looks like yes (used in squirrelchat?)

Unmaintained clients

Language Author Link Notes
Python @PendragonLore https://github.com/PendragonLore/shinkei Owner said it's unmaintained
Java @queer https://github.com/queer/singyeong-java-client I don't write much Java anymore S:
Typescript @alula https://github.com/KyokoBot/node-singyeong-client Repo gone

Credit

신경 was inspired by sekitsui, a project by the Ayana developers. 신경 was developed due to sekitsui seemingly having halted development (no release as far as I'm aware, no repo activity in the last 1-2 years).

WARNING

신경 is ALPHA-QUALITY software. The core functionality works, but there's no guarantee that it won't break, eat your cat, ... Use at your own risk!

Configuration

Configuration is done via environment variables, or via a custom configuration file. See config.exs for more information about config options.

Custom config files

Sometimes, it's necessary to include custom configuration files - such as for something that the environment variables don't cover. In such a case, you can add a custom.exs file to config/ that includes the custom configuration you want / need.

Example: Prove that custom config works:

use Mix.Config

IO.puts "Loading some cool custom config :blobcatcooljazz:"

Example: Always have debug-level logging, even in prod mode:

use Mix.Config

config :logger, level: :debug

Plugins

Plugins belong in a directory named plugins at the root directory. See the plugin API and the example plugin for more info.

Clustering

신경 is capable of discovering cluster members automatically, using libcluster and the gossip strategy by default.

What exactly is it?

신경 is a metadata-oriented message bus + message queue + HTTP proxy. Clients connect over a websocket, and can send messages, queue messages, and send HTTP requests that can be routed to clients based on client metadata.

Metadata-oriented?

신경 clients are identified by three factors:

  1. Application id.
  2. Client id.
  3. Client metadata.

When sending messages or HTTP requests over 신경, you do not choose a target service instance directly, nor does 신경 choose for you. Rather, you specify a target application and a metadata query. 신경 will then run this query on all clients under the given application, and choose one that matches to receive the message or request.

For example, suppose you wanted to let users who had opted-in to a beta program use beta features, but not all users. You could express this as a 신경 query, and say something like "send this message to some service in the backend application where version_number >= 2.0.0."

Of course, something like that is easy, but 신경 lets you do all sorts of things easily. For example, suppose you had a cluster of websocket gateways that users connected to and received events over. Instead of having to know which gateway a user is connected to, you could trivially express this as a 신경 query - "send this message to a gateway node that has 123 in its connected_users metadata." Importantly, sending messages like this is done in exactly the same way as sending any other message. 신경 tries to make it very easy to express potentially-complicated routing with the same syntax as a simple "send to any one service in this application group."

Do I need to know exact client IDs to send messages?

No. You should not try to route to a specific 신경 client by id; instead you should be expressing a metadata query that will send to the client you want. Generally speaking, clients should be capable of running statelessly, or you should use metadata to route messages effectively.

Do I need sidecar containers if I'm running in Kubernetes?

Nope.

Does it support clustering / multi-master / ...?

신경 has masterless clustering support.

Why should I use this?

  • No need for Kubernetes or something similar - anything that can speak websockets is a valid 신경 client.
  • No configuration. 신경 is meant to be "drop in and get started" - a few options exist for things like authentication, but beyond that, no configuration should be needed (at least to start out).
  • Fully dynamic. 신경 is meant to work well with clients randomly appearing and disappearing (ex. browser clients when using 신경 as a websocket gateway).
  • No sidecars.
  • Choose where messages / requests are routed at runtime; no need to bake exact targets into your application.
  • Service discovery without DNS.
  • Service discovery integrated into HTTP proxying / message sending.

Why should I NOT use this?

  • Query performance might be unacceptable.
  • Websockets might not be acceptable.
  • Development is still fairly early-stage; the alpha-ish quality of it may be nonviable.

Why make this?

Metadata-based routing is useful for all sorts of things:

  • Storing documentation about what messages your services send/recv, what REST endpoints they expose, ... and querying on it to route to a service that accepts a specific format of a specific message.
  • Sending a message to a subset of clients without doing a full pubsub and relying on clients to drop them properly.
  • Discord bot message-passing between services. No more pubsub or whatever, just "send this message to the service where 123 in guild_ids."
  • Websocket gateway. "Send this message to the client where user_id = 123."
  • Container scheduling.
  • Monitoring host stats.
  • Routing messages to an audit-logging service and a handler service at the same time.
  • Message queues that can only dispatch messages when a client is capable (ex. "dispatch this message from the queue to a client where latency < 10")
  • Anything you can think of!

In general, 신경 can get messages to the right place with some very complicated conditions very easily.

Why Elixir? Why not Go, Rust, Java, ...?

I like Elixir 👍 Elixir is well-suited to the use-case of message-queuing, and imo is a lot friendlier to write scalable messaging code in with little effort relative to any of the above-mentioned languages.

Why using Phoenix? Why not just use Cowboy directly?

Phoenix's socket abstraction is really really useful. Also I didn't want to have to build eg. HTTP routing from scratch; Phoenix does a great job of it already so no need to reinvent the wheel. While it is possible to use Plug or a similar library on top of Cowboy or another HTTP server, I just liked the convenience of getting it all out-of-the-box with Phoenix and being able to focus on writing my application-level code instead of setting up a ton of weird plumbing.

How do I write my own client for it?

Check out PROTOCOL.md.

How does it work internally?

The code is intended to be pretty easy to read. The general direction that data flows is something like:

client -> decoder -> gateway -> dispatch         -> process and dispatch response
                             -> identifier       -> allow or reject connection
                             -> metadata updater -> apply or reject metadata updates

In terms of module structure, the way things go is something like:

|-> <phx/cowboy code>
|           |
|           V
|   SingyeongWeb.Transport.Raw
|           |
|           V
|   Singyeong.Gateway
|           |
|           |------------------------------------|-----------------------------------------|
|           V                                    V                                         V
|   Singyeong.Gateway.Handler.Identify   Singyeong.Gateway.Handler.DispatchEvent   Singyeong.Gateway.Handler.Heartbeat
|           |                                    |                                         |
|           V                                    V                                         V
|   Singyeong.Gateway                    Singyeong.Gateway.Dispatch                Singyeong.Store ---------|
|           |                                    |                                                          |
|           V                     |--------------|-----------------|-----------------------------|          |
|-- SingyeongWeb.Transport.Raw    |              V                 |                             |          |
            ^                     V      (METADATA_UPDATE)         V                             V          |
            |        Singyeong.Gateway   Singyeong.Store   Singyeong.MessageDispatcher   Singyeong.Queue    |
            |----------------|                                     |                             |          |
            |                                                      |                             |          |
            |------------------------------------------------------|-----------------------------|----------|

Additionally, I aim to keep the server fairly small, ideally <5k LoC, but absolutely <10kLoC no matter what.. At the time of writing, the server is ~3.600 LoC:

git:(master) X | ->  tokei lib/
===============================================================================
 Language            Files        Lines         Code     Comments       Blanks
===============================================================================
 Elixir                 48         4558         3621          267          670
===============================================================================
 Total                  48         4558         3621          267          670
===============================================================================
git:(master) X | ->

How does querying / messaging work?

When you send a message, the server inspects its target (ie. its metadata query) and queries its own internal metadata store to find clients that can be routed to that match said query. In the case of a multi-node setup, each node is only aware of its own clients, and thus metadata queries are a parallel RPC across the cluster.

A very basic query looks like this:

{
  "application": "api",
  "ops": [],
}

That is, "route this message to any client that's a member of the api application." More-complex queries may do things like specifying constraints based on latency:

{
  "application": "api",
  "ops": [
    {
      "path": "/latency/http",
      "op": "$lte",
      "to": {"value": 100},
    }
  ],
}

That is, "route this message to any client that's a member of the api application, where the value at that application's /latency/http key is less than or equal to 100."

As metadata is arbitrary JSON -- the only restriction being that the top-level JSON is an object -- you can query effectively-infinitely-nested structures. You can query paths with a syntax inspired by JSON Patch.

For more about how the query language works, see the relevant docs

Delivery guarantees

Messages are delivered at-least-zero times. That is, speaking broadly, your messages will be delivered exactly once, but there are cases (network issues etc.) that can lead to either no delivery, or more than one delivery, of the same message.

What about test coverage and the like?

You can run tests with mix test. Note that the plugin tests WILL FAIL unless you set up the test plugin in priv/test/plugin/. If you don't want to deal with the plugin-related code when running tests (tho you REALLY should care...), you can skip those tests by running mix test --exclude plugin.

Note that the HTTP proxying tests use an echo server I wrote (echo.amy.gg), rather than using a locally-hosted one. If you don't want to run these tests, set the DISABLE_PROXY_TESTS env var.

Security

Note that there is no ratelimit on authentication attempts. This means that a malicious client can constantly open connections and attempt passwords until it gets the correct one. It is highly recommended that you use a very long, probably-very-complicated password in order to help protect against this sort of attack.

What is that name?

신경 means nerve, and since the nervous system is how the entire body communicates, it seemed like a fitting name for a messaging system. I considered naming this something like 등뼈 (deungppyeo, "spine"/"backbone") or 회로망 (hoelomang, "network") or even 별자리 (byeoljali, "constellation), but I figured that 신경 would be easier for people who don't know Korean to pronounce, as well as being easier to find from GitHub search.

singyeong's People

Contributors

cyyynthia avatar dependabot-preview[bot] avatar dependabot-support avatar dependabot[bot] avatar kpodp0ra avatar natanbc avatar pendragonlore avatar queer avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

singyeong's Issues

Query engine v2

The current query engine is... problematic, to say the least. It’s pretty incomprehensible, can be quite slow (O(N) client scans...), and is difficult to add new features onto. Query engine v2 aims to solve all of these and more:

  • Rewrite it to be cleaner and better-tested.
  • Implement proper indexing over client metadata for faster lookups (should be able to get O(log2(N)) or even O(1) in some cases).
  • Implement selectors (ex. Ie “select from clients where x = true by min(latency)” sort of support for routing).
  • Clean up some issues that exist, such as only being able to have one key / operator per-op. (See #117)

Fake services for proxying non-신경-aware services

Basically, not everything is or can be aware of 신경 -- this includes important things like databases, Redis, ... To solve this, 신경 should support "fake" application clients (perhaps configurable via HTTP API?) that can be proxied to despite not being a "real" 신경 client.

Better custom config support

Right now, you have to mount a file into the image that overrides the existing configs; it should be easier than this.

Round-robin message routing

If more than 1 target matches a given routing query, then the message will be sent to a random target. We should support round-robin and possibly other strategies.

The connection is not being closed when an error happens.

close_with_payload seems to be broken, the connection is not being closed when heartbeat timeouts.

  singyeong <- 5 { client_id: 'hello-owo' } +45s
  singyeong -> { ts: 1573317517641, t: null, op: 6, d: { client_id: 'hello-owo' } } +1ms
  singyeong <- 5 { client_id: 'hello-owo' } +45s
  singyeong -> { ts: 1573317562683, t: null, op: 6, d: { client_id: 'hello-owo' } } +0ms
  singyeong <- 5 { client_id: 'hello-owo' } +2h
  singyeong -> {
  ts: 1573326395214,
  t: null,
  op: 3,
  d: { error: 'heartbeat took too long' }
} +0ms
  singyeong <- 5 { client_id: 'hello-owo' } +45s
  singyeong -> {
  ts: 1573326440253,
  t: null,
  op: 3,
  d: { error: 'heartbeat took too long' }
} +1ms

feature: Proper E2E testing

Considerations:

  • Where / how?
  • What client to use?
  • Potential cost, if using cloud:tm: VMs...
  • How to ensure coverage?

Erlang cluster discovery helpers

This is one of the cases where #38 wouldn't actually be enough -- It'd be nice to be able to use singyeong to bootstrap an Erlang node cluster from the ground up.

Allow clients to configure interaction limits

Things like "can only receive so many requests/messages per period," basically just setting limits on how messages are delivered or requests are proxied, things like forcing SSL, ..., so that each client can control how the rest of the world can interact with it. This would require some possibly-substantial per-node state-tracking, mainly to survive during a failover scenario.

Better query language

The "query language" used for message routing is fairly clunky and missing some meaningful features; we should come up with a better way of doing things.

Allow more intelligent message fanout.

This is a part of #138.

I'd never really thought a tonne about how tags were used, but then @panklipcio had raised this idea to me in Discord:

tags for subjective event fanout

While this can still be emulated -- probably actually overall better -- via metadata, it opens up the idea of allowing a message fanout to be sent to more than one app id at a time. Think ex. "send this message to backend worker to process it, and also send it to the audit service to be logged" in one send, rather than the client having to send multiple messages (that could get lost!).

Only fetch necessary metadata keys when querying

Right now we just fetch everything and convert it into a map; this is clearly sub-optimal for performance. Only fetching the necessary keys from a client's metadata should have a good performance improvement.

External authentication

Right now, 신경works fine for the "internal message bus/service mesh" use-case. However, some people have expressed interest in using it as an API gateway. In this case, it would be useful to add support for an external authentication method so that people can authenticate against their own credential stores or whatever.

To consider:

  • Auth request backends. Should there be adapters for everything known to man, or require users to expose a RESTful auth service of some sort?
  • Auth request/response format. Should we handle everything? Should it just be a RESTful JSON exchange?
  • Permission levels. Separate into REST/WS? What about restricting further (ex. an extra "restricted mode" layer of sorts?) How should this be handled in the context of the previous two questions?

Binary protocol

Just because JSON parsing all the time is inefficient af. Maybe ETF?

Proper plugin system

Self-explanatory. I want to have a real plugin system because that would allow for a LOT of things to be implemented without cluttering up the core.

  • Plugin manifest
  • Capabilities exposed and validated via manifest
  • Plugin loading
  • Mix task to package plugins
  • Native code integration
  • Proper dependency packaging
  • Plugin dependencies
  • Custom plugin events
  • Metadata store view
  • Custom metadata store implementation
  • Full event processing
  • Custom authorization processing
  • Fire events directly as well as in response to incoming events
  • Custom REST endpoints

feature: Load natives in a platform-dependent manner

Plugin natives are currently assumed to be targeting the native platform. This works fine in the case of everything running in Docker, but may be complicated by natives being built on one platform and run on another. It should be possible to have plugin natives loaded from a different file depending on the platform.

Ratelimiting?

Pretty self-explanatory; might want a configurable way to ratelimit REST/WS payloads.

Support request/response messaging

Use case: I'd like to be able to say, for example, get me the latency of all servers in the eu region, and have singyeong figure out everything for me. This would be extremely useful for real-time monitoring or metrics for example, or even just service discovery for users. (E.g. a realm list in a MMORPG.)

As a workaround, you could configure some session ID (e.g. a GUID) as metadata and then say respond to [guid] with the latency for servers in the eu region. The latency would be returned as another query, e.g. send [x]ms to the server marked with session id [guid].

Ability to drop dispatches which can't be routed

In development environments, it's likely only one instance of services are running at a time. In these circumstances, it would be desirable to drop dispatches which can't be routed in order to prevent cascading failures in high-throughput scenarios (e.g. one service goes down for a few moments, and services which route on that service get disconnected, and so on so forth.)

I imagine the above scenario would also happen in production, but it would be less likely to happen as there are likely multiple instances of a service running.

Consistently-hashed message sending

Pretty much just the title. It'll be VERY convenient to have a "ensure this message is sent to the same target node, assuming the query returns the same set of nodes" function, to ensure certain things (example: message triggers an API request, 'consistent hash' it to deal with ratelimits in-memory-only)

Stop closing the socket on all :invalid payloads

Man idk what I was thinking when I implemented this.

Some stuff (payload can't route, invalid metadata update, ...) should send an :invalid but NOT close, whereas some other stuff (malformed payload, ...) should close with either :invalid or :goodbye.

Support initial queues in identify payload

Automatically performs QUEUE_REQUEST for the specified queues.

Example payload:

{
  "client_id": "19274eyuholdis3vhynurtlofkbhndvhvqkl34wjgyhnewri",
  "application_id": "my-cool-application",
  "queues": [
    "my-queue-name"
  ]
}

Proper message queueing

Properly queue messages, ie don't fire-and-forget in the case of ex. waiting on a service to reconnect

Things to consider:

  1. Do we have timeouts? How do we communicate them to the sender?
  2. We can't just naively queue, as we have ex. metadata queries to think about. Do we just pop it from the queue immediately?
  3. Queue persistence
  4. Clustering support

Fix implementation of metadata list storage

Lists are dumb -- Mnesia can't really support them nicely because not O(1), so to work around this, singyeong currently stores lists as map keys. However, this destroys ordering and also makes lists into mapsets. This can theoretically be fixed by list -> list with indexes -> into map, and possibly an inverted index for lookups.

Proper distribution

Yes, this means implementing the "heck off" opcode :C

TODO:

  • Choose a clustering solution
  • Figure out load-balancing strategy
  • Querying across all nodes
  • Sending messages / proxied HTTP requests across all nodes
  • Actually send heck off to clients
  • Unit testing

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.