GithubHelp home page GithubHelp logo

conduit_amqp's Introduction

ConduitAMQP

An AMQP adapter for Conduit.

Installation

This package can be installed as:

  1. Add conduit_amqp to your list of dependencies in mix.exs:

    def deps do
      [{:conduit_amqp, "~> 0.6.3"}]
    end
  2. Ensure conduit_amqp is started before your application:

    def application do
      [applications: [:conduit_amqp]]
    end

Configuring the Adapter

# config/config.exs

config :my_app, MyApp.Broker,
  adapter: ConduitAMQP,
  url: "amqp://my_app:[email protected]"

# Stop lager redirecting :error_logger messages
config :lager, :error_logger_redirect, false

# Stop lager removing Logger's :error_logger handler
config :lager, :error_logger_whitelist, [Logger.ErrorHandler]

For the full set of options, see ConduitAQMP.

Configuring Exchanges

You can define exchanges with the exchange macro in the configure block of your Broker. The exchange macro accepts the name of the exchange and options for the exchange.

Options

  • :type - Either :topic, :fanout, :direct, or :headers. Defaults to :topic.
  • :durable - If set, keeps the Exchange between restarts of the broker. Defaults to false.
  • :auto_delete - If set, deletes the Exchange once all queues unbind from it. Defaults to false.
  • :passive - If set, returns an error if the Exchange does not already exist. Defaults to false.
  • :internal - If set, the exchange may not be used directly by publishers. Defaults to false.

See exchange.declare for more details.

Example

defmodule MyApp.Broker do
  use Conduit.Broker, otp_app: :my_app

  configure do
    exchange "my.topic", type: :topic, durable: true
  end
end

Configuring Queues

You can define queues with the queue macro in the configure block of your Broker. The queue macro accepts the name of the queue and options for the exchange.

Options

  • :durable - If set, keeps the Queue between restarts of the broker. Defaults to false.
  • :auto_delete - If set, deletes the Queue once all subscribers disconnect. Defaults to false.
  • :exclusive - If set, only one subscriber can consume from the Queue. Defaults to false.
  • :passive - If set, raises an error unless the queue already exists. Defaults to false.
  • :from - A list of routing keys to bind the queue to.
  • :exchange - Name of the exchange used to bind the queue to the routing keys.

See queue.declare for more details.

Example

defmodule MyApp.Broker do
  use Conduit.Broker, otp_app: :my_app

  configure do
    queue "my.queue", from: ["#.created.user"], exchange: "amq.topic", durable: true
  end
end

Configuring a Subscriber

Inside an incoming block for a broker, you can define subscriptions to queues. Conduit will route messages on those queues to your subscribers.

defmodule MyApp.Broker do
  incoming MyApp do
    subscribe :my_subscriber, MySubscriber, from: "my.queue"
    subscribe :my_other_subscriber, MyOtherSubscriber,
      from: "my.other.queue",
      prefetch_size: 20
  end
end

Options

  • :from - Accepts a string or function that resolves to the queue to consume from. Defaults to the name of the route if not specified.
  • :prefetch_size - Size of prefetch buffer in octets. Defaults to 0, which means no specific limit. This can also be configured globally by passing this same option when configuring your Broker.
  • :prefetch_count - Number of messages to prefetch. Defaults to 0, which means no specific limit. This can also be configured globally by passing this same option when configuring your Broker.
  • :consumer_tag - Specifies the identifier for the consumer. The consumer tag is local to a channel, so two clients can use the same consumer tags. If this field is empty the server will generate a unique tag.
  • :no_local - If the no-local field is set the server will not send messages to the connection that published them. Defaults to false.
  • :no_ack - If this field is set the server does not expect acknowledgements for messages. That is, when a message is delivered to the client the server assumes the delivery will succeed and immediately dequeues it. Defaults to false.
  • :exclusive - Request exclusive consumer access, meaning only this consumer can access the queue. Defaults to false.
  • :nowait - If set, the server will not respond to the method. The client should not wait for a reply method. If the server could not complete the method it will raise a channel or connection exception. Defaults to false.
  • :arguments - A set of arguments for the consume. Defaults to [].

Note: It's highly recommended to set :prefetch_size or :prefetch_count to a non-zero value to limit the memory consumed when a queue is backed up.

See basic.qos and basic.consume for more details on options.

Configuring a Publisher

Inside an outgoing block for a broker, you can define publications to exchanges. Conduit will deliver messages using the options specified. You can override these options, by passing different options to your broker's publish/3.

defmodule MyApp.Broker do
  outgoing do
    publish :destination_route,
      to: "my.routing_key",
      exchange: "amq.topic"
    publish :other_destination_route,
      to: "my.other.routing_key",
      exchange: "amq.topic"
  end
end

Options

  • :to - The routing key for the message. If the message already has its destination set, this option will be ignored.
  • :exchange - The exchange to publish to. This option is required.
  • :publisher_confirms - This configures publisher confirms. Should be one of :no_confirmation (the default), :wait (if it should just return an :timeout atom on failure) or :die (raises an exception on timeout).
  • :publisher_confirms_timeout - The timeout in milliseconds for the server acknowledgement. Should be :infinity for no timeout (the default) or an integer number of milliseconds.

See basic.publish for more details.

Example usage

%Message{}
|> put_body(%{"my" => "message"})
|> Broker.publish(:destination_route)

Architecture

ConduitAQMP architecture

When ConduitAMQP is used as an adapter for Conduit, it starts ConduitAMQP as a child supervisor. ConduitAMQP starts:

  1. ConduitAQMP.ConnPool - Creates and supervises a pool of AMQP connections.
  2. ConduitAMQP.PubSub - Creates and supervises ConduitAMQP.PubPool and ConduitAMQP.SubPool.
  3. ConduitAMQP.Subscribers - A supervisor for subscribers that process messages.

conduit_amqp's People

Contributors

b1az avatar blatyo avatar coladarci avatar doughsay avatar riosgabriel avatar thalesmg avatar youalreadydid avatar zblanco avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

conduit_amqp's Issues

Application goes down on startup when it can not connect to rabbitmq

I'm trying to configure conduit in a project. It works fine for most cases, but one of the problems I'm having is that once the connection with rabbitmq can't be established for some reason, the application goes down. The issue doesn't happen when the connection is lost after the application is already up.

The error occurs at ConduitAMQP.PubSub module, line 23, when it tries to get a connection from the pool but receives an error. I'm not sure, but this step seems needed only if the 'cofigure' macro has been used. One possible solution to avoid this problem, at least on this case, would be to verify if this step must be executed by changing the code as above:

Before:

    case ConduitAMQP.with_conn(broker, &Channel.open/1) do
      {:ok, chan} -> configure(chan, topology)
    end

After:

    unless Enum.empty?(topology) do 
      {:ok, chan} = ConduitAMQP.with_conn(broker, &Channel.open/1)
      configure(chan, topology)
    end

This solution solves my problem, but I'm not sure it's the best solution, if the configure macro it's set up the problem will persist and I don't know how would be the best way to deal with this.

Request for guidance: direct-reply-to/RPC-style message flows

This library seems incredible after spending an hour or two with it last week! ๐ŸŽ‰ ๐ŸŽŠ

My employer, @CityBaseInc, is considering adopting this project as part of our event-bus architecture, but in its current iteration, we are currently taking advantage of the request-response message flow that is possible in NATS. (This is not required reading for the purposes of this discussion.) We're using this model for RPC purposes, and RabbitMQ has similarly capable features that we'd like to enable in our chosen client library in order to perform a more complete comparison between NATS and RabbitMQ.

Can you please mull this over and suggest likely areas of the codebase or API contract that aren't currently compatible if this message flow was to be fully supported at the same high quality of the existing feature set? We'd be willing to (at very least) spike on an implementation after some pointers, if that kind of contribution would be welcome. I hope not to maintain a private fork or dip down into the underlying amqp library with any regularity, but I also realize this is not a universal use-case.

The immediate concern I have myself is what existing or new component might handle and store what amounts to a transient "mailbox switchboard". This would provide the final leg of an incoming reply's journey, from the supervised Conduit subscriber to a sibling Elixir process that is currently awaiting a reply message from an earlier publish. I'm not sure if that can or should live in the local subscriber module. I'm also not even sure how I would start to implement that without digging deep into the guts of conduit_amqp. I was able to make this work at least partially successfully with the underlying amqp library, so that's encouraging.

We also need to be able to subscribe to the "magic" queue name mentioned in the linked document above. This requires us to pass no_ack: true to AMQP.Basic.consume. I think I had that working with a conduit_amqp diff similar to the following:

diff --git a/lib/conduit_amqp/sub.ex b/lib/conduit_amqp/sub.ex
index 7bf4216..1a14c82 100644
--- a/lib/conduit_amqp/sub.ex
+++ b/lib/conduit_amqp/sub.ex
@@ -60,9 +60,10 @@ defmodule ConduitAMQP.Sub do
   def handle_info(:connect, %{status: :disconnected, broker: broker, name: name, opts: opts} = state) do
     case ConduitAMQP.with_conn(broker, &Channel.open/1) do
       {:ok, chan} ->
+        consume_opts = Keyword.get(opts, :consume_opts, [])
         Process.monitor(chan.pid)
         Basic.qos(chan, opts)
-        Basic.consume(chan, opts[:from] || Atom.to_string(name))
+        Basic.consume(chan, opts[:from] || Atom.to_string(name), nil, consume_opts)
         Logger.info("#{inspect(self())} Channel opened for subscription #{inspect(name)}")
 
         {:noreply, %{state | chan: chan, status: :connected}}

I wasn't immediately successful in declaring a subscriber for the amq.rabbitmq.reply-to pseudo-queue mentioned in the linked RabbitMQ documentation. I'm not able to recall what specific error I ran into, and I unfortunately didn't commit the code that generated it, but I also had trouble publishing a Message with the reply-to header included. Odds are pretty good that it may have stemmed from the prior issue.

Thanks for any information or clarity you can provide!

Nack and requeue: false?

I currently have a nack in my subscriber process function, and it appears the message is requeued.

I cannot find any documentation about how to set requeue to false. I did find this:

Basic.reject(chan, props.delivery_tag)

    case broker.receives(name, message) do
      %Message{status: :ack} ->
        Basic.ack(chan, props.delivery_tag)

      %Message{status: :nack} ->
        Basic.reject(chan, props.delivery_tag)
    end

And found an example of a Basic.reject here in amqp package:

:ok = Basic.reject channel, tag, requeue: false

I'm wondering if this is possible with conduit without forking/patching?

Is it possible to pass in "arguments" for exchanges?

I'm using a dedupe plugin for RMQ and am struggling to pass in some required arguments when declaring an exchange. Any suggestions?

    exchange "my_exchange",
      type: :"x-message-deduplication",
      arguments: [{"x-cache-size", :short, 100}],
      durable: true

The dedupe plugin requires an argument to be present for x-cache-size, but cannot figure out even if conduit_amqp passes arguments? The documentation around exchange declare links to https://www.rabbitmq.com/amqp-0-9-1-reference.html#exchange.declare instead of documentation for conduit implementation.

From my understanding, conduit is using amqp under the hood?

https://github.com/conduitframework/amqp#types-of-arguments-and-headers

There are examples of using arguments. However, no custom arguments are allowed (exchange declare only allows alternate-exchange).

I just wanted to confirm if that's the issue I'm running into and what would be the best way to modify this to my tastes. Are you passing arguments through to amqp and so I simply need to fork/modify that?

Thanks.

Unexplained ArgumentError

Hello, I'm having trouble diagnosing where this error is coming from and the stack trace isn't giving any clues :/ Any chance this looks familiar to you?

It's occuring when I publish jobs, and only for some of the jobs. I assume there's something wrong with the formatting of the job or something, and it's just swallowing the error somewhere along the way?

[error] ** (ArgumentError) argument error
[info] Processed message from data_digest.jobs in 759ms
[error] Task #PID<0.657.0> started from #PID<0.584.0> terminating
** (ArgumentError) argument error
    :erlang.byte_size(400)
    (data_digest) lib/data_digest_queue/subscribers/jobs_subscriber.ex:21: DataDigestQueue.JobsSubscriber.process/2
    (conduit) lib/conduit/plug/log_incoming.ex:26: Conduit.Plug.LogIncoming.call/3
    (conduit_amqp) lib/conduit_amqp/task.ex:16: ConduitAMQP.Task.run/6
    (elixir) lib/task/supervised.ex:90: Task.Supervised.invoke_mfa/2
    (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
Function: &ConduitAMQP.Task.run/6
    Args: [DataDigestQueue.Broker, %AMQP.Channel{conn: %AMQP.Connection{pid: #PID<0.595.0>}, pid: #PID<0.654.0>}, :jobs, "data_digest.jobs", <<31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 109, 81, 193, 78, 195, 48, 12, 253, 149, 40, 210, 38, 144, 182, 102, 155, 64, 136, 106, 45, 59, 115, 224, 194, 145, 161, 201, 75, 188, 214, 83, 155, 148, 36, 101, ...>>, %{app_id: "data_digest", cluster_id: :undefined, consumer_tag: "amq.ctag-eHvPPCckn1r9NHb7Ax31jg", content_encoding: "gzip", content_type: "application/json", correlation_id: "93d21c60-0169-4aed-9164-ab09e1da06b8", delivery_tag: 1, exchange: "amq.topic", expiration: :undefined, headers: [{"created_at", :longstr, "2019-05-06T11:41:08.224457Z"}], message_id: :undefined, persistent: false, priority: :undefined, redelivered: true, reply_to: :undefined, routing_key: "data_digest.jobs", timestamp: :undefined, type: :undefined, user_id: :undefined}]
[error] ** (ArgumentError) argument error
[info] Processed message from data_digest.jobs in 829ms
13:11:58.000 [warning] lager_error_logger_h dropped 126 messages in the last second that exceeded the limit of 50 messages/sec
[error] Task #PID<0.658.0> started from #PID<0.584.0> terminating
** (ArgumentError) argument error
    :erlang.byte_size(400)
    (data_digest) lib/data_digest_queue/subscribers/jobs_subscriber.ex:21: DataDigestQueue.JobsSubscriber.process/2
    (conduit) lib/conduit/plug/log_incoming.ex:26: Conduit.Plug.LogIncoming.call/3
    (conduit_amqp) lib/conduit_amqp/task.ex:16: ConduitAMQP.Task.run/6
    (elixir) lib/task/supervised.ex:90: Task.Supervised.invoke_mfa/2
    (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
Function: &ConduitAMQP.Task.run/6
    Args: [DataDigestQueue.Broker, %AMQP.Channel{conn: %AMQP.Connection{pid: #PID<0.595.0>}, pid: #PID<0.654.0>}, :jobs, "data_digest.jobs", <<31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 109, 81, 193, 78, 195, 48, 12, 253, 149, 40, 210, 38, 144, 182, 102, 155, 64, 136, 106, 45, 59, 115, 224, 194, 145, 161, 201, 75, 188, 214, 83, 155, 148, 36, 101, ...>>, %{app_id: "data_digest", cluster_id: :undefined, consumer_tag: "amq.ctag-eHvPPCckn1r9NHb7Ax31jg", content_encoding: "gzip", content_type: "application/json", correlation_id: "fc7f8bf9-57cf-464a-8464-ac2f00e6204e", delivery_tag: 2, exchange: "amq.topic", expiration: :undefined, headers: [{"created_at", :longstr, "2019-05-06T11:45:01.128109Z"}], message_id: :undefined, persistent: false, priority: :undefined, redelivered: true, reply_to: :undefined, routing_key: "data_digest.jobs", timestamp: :undefined, type: :undefined, user_id: :undefined}]
13:11:58.001 [error] ** Task <0.658.0> terminating
** Started from <0.584.0>
** When function  == fun Elixir.ConduitAMQP.Task:run/6
**      arguments == ['Elixir.DataDigestQueue.Broker',#{'__struct__' => 'Elixir.AMQP.Channel',conn => #{'__struct__' => 'Elixir.AMQP.Connection',pid => <0.595.0>},pid => <0.654.0>},jobs,<<"data_digest.jobs">>,<<31,139,8,0,0,0,0,0,0,19,109,81,193,78,195,48,12,253,149,40,210,38,144,182,102,155,64,136,106,45,59,115,224,194,145,161,201,75,188,214,83,155,148,36,101,98,85,254,157,180,171,208,64,28,34,91,207,246,243,243,75,199,247,70,125,237,60,214,77,5,30,121,202,187,9,163,3,83,224,33,161,8,187,196,209,25,89,206,22,108,18,182,154,177,117,185,204,95,240,196,158,225,19,94,165,165,198,51,139,141,113,107,17,11,67,67,91,13,49,18,29,140,101,61,9,35,125,197,56,18,197,206,138,242,75,22,115,96,165,197,67,182,229,93,215,119,37,165,175,171,93,107,171,16,182,60,31,49,13,53,134,176,22,240,51,118,51,86,156,7,91,192,25,173,219,73,211,106,31,2,235,33,119,59,110,18,227,170,40,10,181,234,117,141,215,136,65,237,5,142,119,79,2,159,113,69,5,58,191,35,197,211,135,25,199,26,168,114,60,125,227,158,234,77,124,39,114,137,52,117,108,148,27,153,200,24,113,131,9,242,247,152,104,213,24,210,254,218,208,210,251,198,165,66,64,67,73,65,190,108,247,253,180,112,8,86,150,98,240,142,188,177,132,238,233,35,147,22,227,148,74,243,108,181,88,62,206,23,119,243,213,61,171,64,23,45,20,152,30,163,231,110,240,124,234,140,245,217,112,226,212,88,133,54,83,232,122,41,13,88,168,163,218,46,204,184,107,247,71,148,191,196,116,221,159,159,141,70,233,127,126,147,135,111,129,101,167,239,26,2,0,0>>,#{app_id => <<"data_digest">>,cluster_id => undefined,consumer_tag => <<"amq.ctag-eHvPPCckn1r9NHb7Ax31jg">>,content_encoding => <<"gzip">>,content_type => <<"application/json">>,correlation_id => <<"fc7f8bf9-57cf-464a-8464-ac2f00e6204e">>,delivery_tag => 2,exchange => <<"amq.topic">>,expiration => undefined,headers => [{<<"created_at">>,longstr,<<"2019-05-06T11:45:01.128109Z">>}],message_id => undefined,persistent => false,priority => undefined,redelivered => true,...}]
** Reason for termination ==
** {#{'__exception__' => true,'__struct__' => 'Elixir.ArgumentError',message => <<"argument error">>},[{erlang,byte_size,[400],[]},{'Elixir.DataDigestQueue.JobsSubscriber',process,2,[{file,"lib/data_digest_queue/subscribers/jobs_subscriber.ex"},{line,21}]},{'Elixir.Conduit.Plug.LogIncoming',call,3,[{file,"lib/conduit/plug/log_incoming.ex"},{line,26}]},{'Elixir.ConduitAMQP.Task',run,6,[{file,"lib/conduit_amqp/task.ex"},{line,16}]},{'Elixir.Task.Supervised',invoke_mfa,2,[{file,"lib/task/supervised.ex"},{line,90}]},{proc_lib,init_p_do_apply,3,[{file,"proc_lib.erl"},{line,249}]}]}
13:11:58.001 [error] CRASH REPORT Process <0.658.0> with 0 neighbours crashed with reason: {#{'__exception__' => true,'__struct__' => 'Elixir.ArgumentError',message => <<"argument error">>},[{erlang,byte_size,[400],[]},{'Elixir.DataDigestQueue.JobsSubscriber',process,2,[{file,"lib/data_digest_queue/subscribers/jobs_subscriber.ex"},{line,21}]},{'Elixir.Conduit.Plug.LogIncoming',call,3,[{file,"lib/conduit/plug/log_incoming.ex"},{line,26}]},{'Elixir.ConduitAMQP.Task',run,6,[{file,"lib/conduit_amqp/task.ex"},{line,16}]},{'Elixir.Task.Supervised',invoke_mfa,2,[{file,"lib/task/supervis..."},...]},...]}
13:11:58.001 [error] Supervisor 'Elixir.DataDigestQueue.Broker.Adapter.Tasks' had child undefined started with {'Elixir.Task.Supervised',start_link,undefined} at <0.658.0> exit with reason {#{'__exception__' => true,'__struct__' => 'Elixir.ArgumentError',message => <<"argument error">>},[{erlang,byte_size,[400],[]},{'Elixir.DataDigestQueue.JobsSubscriber',process,2,[{file,"lib/data_digest_queue/subscribers/jobs_subscriber.ex"},{line,21}]},{'Elixir.Conduit.Plug.LogIncoming',call,3,[{file,"lib/conduit/plug/log_incoming.ex"},{line,26}]},{'Elixir.ConduitAMQP.Task',run,6,[{file,"lib/conduit_amqp/task.ex"},{line,16}]},{'Elixir.Task.Supervised',invoke_mfa,2,[{file,"lib/task/supervis..."},...]},...]} in context child_terminated

Using with fanout exchange

Hi, I'm trying to use conduit with simple fanout exchanges in rabbitmq.

This line: https://github.com/conduitframework/conduit_amqp/blob/master/lib/conduit_amqp/topology.ex#L38

Always binds the defined queues to the exchange with a routing key. In fact, it requires a routing key to be defined before it even attempts to bind. But fanout exchanges ignore routing keys (according to what I've read).

So I have a broker like this:

configure do
  exchange "example.exchange", type: :fanout, durable: true

  queue "my.queue", exchange: "example.exchange", from: "ignored", durable: true
end

I have to put something in the :from key in the config, even though it will be ignored.

I feel like it should be possible to not specify that option, and have the binding happen anyway for fanout exchanges, but I can't think of a clean way to make that happen in this repo.

Thoughts?

not compatible with conduit 0.12.1?

I tried upgrading from conduit 0.11.0 to 0.12.1 and I get this error:

** (Mix) Could not start application api: API.Application.start(:normal, []) returned an error: shutdown: failed to start child: API.Broker
    ** (EXIT) shutdown: failed to start child: API.Broker.Adapter
        ** (EXIT) shutdown: failed to start child: API.Broker.Adapter.PubSub
            ** (EXIT) an exception was raised:
                ** (Protocol.UndefinedError) protocol String.Chars not implemented for {:<>, [context: API.BrokerHelper, import: Kernel, line: 6], ["noreaga.content/stream/activity", ".error"]}. This protocol is implemented for: Atom, BitString, Date, DateTime, Decimal, Ecto.Date, Ecto.DateTime, Ecto.Time, Float, Integer, List, Money, NaiveDateTime, Postgrex.Copy, Postgrex.Query, Postgrex.Stream, Time, URI, Version, Version.Requirement
                    (elixir) /home/ubuntu/bob/tmp/835867cc00c6933562caf44708cfd8f2/elixir/lib/elixir/lib/string/chars.ex:3: String.Chars.impl_for!/1
                    (elixir) /home/ubuntu/bob/tmp/835867cc00c6933562caf44708cfd8f2/elixir/lib/elixir/lib/string/chars.ex:22: String.Chars.to_string/1
                    (conduit_amqp) lib/conduit_amqp/topology.ex:38: anonymous fn/2 in ConduitAMQP.Topology.configure_queues/2
                    (elixir) lib/enum.ex:737: Enum."-each/2-lists^foreach/1-0-"/2
                    (elixir) lib/enum.ex:737: Enum.each/2
                    (conduit_amqp) lib/conduit_amqp/topology.ex:17: ConduitAMQP.Topology.configure/2
                    (conduit_amqp) lib/conduit_amqp/pub_sub.ex:24: ConduitAMQP.PubSub.init/1
                    (stdlib) supervisor.erl:294: :supervisor.init/1

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.