Code Monkey home page Code Monkey logo

vice's Introduction

Go channels at horizontal scale Build Status

  • Use Go channels transparently over a messaging queue technology of your choice (Currently NATS, Redis or NSQ, Amazon SQS)
  • Swap vice.Transport to change underlying queueing technologies transparently
  • Write idiomatic Go code instead of learning queue specific APIs
  • Develop against in-memory implementation before putting it into the wild
  • Independent unit tests (no need for running queue technology)

PROJECT STATUS: v1 released

Usage

This code receives names on the |names| queue, and sends greetings on the |greetings| queue:

// get a Go channel that will receive messages on the
// |names| queue
names := transport.Receive("names")

// get a Go channel that will send messages on the
// |greetings| queue
greetings := transport.Send("greetings")

// respond to |names| messages with |greetings|
for name := range names {
	greetings <- []byte("Hello " + string(name))
}
  • The code above is illustrative, be sure to read the design patterns
  • Always stop the Transport, some technologies register and deregister their interest in the queues (this means trapping signals and gracefully shutting down services before exiting)
  • Use Send and Receive methods to get channels, which you can then use as normal
  • Be sure to always handle the ErrChan() error channel to make sure the underlying queue technology is healthy

Quick start guide

  • Write your services with unit tests using normal Go channels (see our design patterns)
  • Install Vice with go get github.com/matryer/vice/...
  • Select a messaging queue technology
  • Build a command to run your service

Read the blog post: Introducing vice: Go channels across many machines

Acknowledgements

Special thanks go to David Hernandez, Jason Hancock and Piotr Rojek for their support on this project.

vice's People

Contributors

cristaloleg avatar heavyhorst avatar imladenovic avatar jsteenb2 avatar matryer avatar nicholasjackson avatar ojizero avatar patcon avatar piotrrojek 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  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  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

vice's Issues

RabbitMQ transport: reconnect

Right now, when connection to broker drops, there is no way to reconnect and restore channels functionality. Also, as I see, chan []bytes created by vice.Transport blocks forever after disconnection.

I can propose pull request (like here), but not at this moment.

What you think?

Project Status

On the front page the documentation states PROJECT STATUS: BETA - Aiming for v1.0 release in October 2017.

When will v1.0 come out (as of June 2018)?

context.Context friendly API

Done() chan struct{}

Done() in context.Context returns a <-chan struct{}, which is not compatible with vice.Transport.

	*Transport does not implement vice.Transport (wrong type for Done method)
		have Done() <-chan struct {}
		want Done() chan struct {}

sqs: data race

rkaufmann@chefkaufmann ..src/github.com/matryer/vice/queues/sqs (git)-[redis] % go test -v -race .
=== RUN   TestTransport
=== RUN   TestTransport/testStandardTransportBehaviour
==================
WARNING: DATA RACE
Write at 0x00c420141350 by goroutine 11:
  runtime.mapassign_faststr()
      /usr/local/go/src/runtime/hashmap_fast.go:598 +0x0
  github.com/matryer/vice/queues/sqs.(*mockSQSClient).ReceiveMessage()
      /home/rkaufmann/go/src/github.com/matryer/vice/queues/sqs/sqs_test.go:65 +0x648
  github.com/matryer/vice/queues/sqs.(*Transport).makeSubscriber.func1()
      /home/rkaufmann/go/src/github.com/matryer/vice/queues/sqs/sqs.go:96 +0xac

Previous read at 0x00c420141350 by goroutine 10:
  runtime.mapaccess1_faststr()
      /usr/local/go/src/runtime/hashmap_fast.go:208 +0x0
  github.com/matryer/vice/queues/sqs.(*mockSQSClient).ReceiveMessage()
      /home/rkaufmann/go/src/github.com/matryer/vice/queues/sqs/sqs_test.go:73 +0x223
  github.com/matryer/vice/queues/sqs.(*Transport).makeSubscriber.func1()
      /home/rkaufmann/go/src/github.com/matryer/vice/queues/sqs/sqs.go:96 +0xac

Goroutine 11 (running) created at:
  github.com/matryer/vice/queues/sqs.(*Transport).makeSubscriber()
      /home/rkaufmann/go/src/github.com/matryer/vice/queues/sqs/sqs.go:90 +0x2e6
  github.com/matryer/vice/queues/sqs.(*Transport).Receive()
      /home/rkaufmann/go/src/github.com/matryer/vice/queues/sqs/sqs.go:64 +0x1b6
  github.com/matryer/vice/vicetest.testStandardTransportBehaviour.func2()
      /home/rkaufmann/go/src/github.com/matryer/vice/vicetest/test.go:77 +0x224

Goroutine 10 (running) created at:
  github.com/matryer/vice/queues/sqs.(*Transport).makeSubscriber()
      /home/rkaufmann/go/src/github.com/matryer/vice/queues/sqs/sqs.go:90 +0x2e6
  github.com/matryer/vice/queues/sqs.(*Transport).Receive()
      /home/rkaufmann/go/src/github.com/matryer/vice/queues/sqs/sqs.go:64 +0x1b6
  github.com/matryer/vice/vicetest.testStandardTransportBehaviour.func2()
      /home/rkaufmann/go/src/github.com/matryer/vice/vicetest/test.go:67 +0x104
==================
=== RUN   TestTransport/testSendChannelsDontBlock
--- FAIL: TestTransport (0.01s)
    --- FAIL: TestTransport/testStandardTransportBehaviour (0.01s)
        testing.go:700: race detected during execution of test
    --- FAIL: TestTransport/testSendChannelsDontBlock (0.00s)
        testing.go:700: race detected during execution of test
=== RUN   TestParseRegion
--- FAIL: TestParseRegion (0.00s)
FAIL
exit status 1
FAIL    github.com/matryer/vice/queues/sqs      0.022s

Cannot control message pulling from the queue

I have this use case:

  • A Redis list is initialized with 3 messages '1', '2' and '3'.
  • A main function iterates forever on a redis receiver.
  • After each received message the goroutine sleeps for 30 seconds.

I would expect that after pulling a single message ('1') - two message will remain on the redis list, but instead - only the last message remains while the second message was already pulled by the redis.makeSubscriber function, and waits in the internal channel instead of in the redis list itself.

This functionality prevents from other machines pulling the message while the first is sleeping.

Is there any way to fix it?

Thanks!

some messages might get dropped due to buffered channels

We use buffered channels for the sender in some queue implementations (nats, redis, sqs).
This increases the performance a lot (#12) but if we stop the transport we might leave some messages in this channel without sending them to the message queue.

Should we flush all remaining messages to the queue if we call Stop() ?

Doesn't build with nats

I get this build error when I try to build after importing "github.com/matryer/vice/queues/nats". ....\pkg\mod\github.com\matryer\[email protected]\queues\nats\nats.go:83:99: cannot use t.natsConn (variable of type *"github.com/nats-io/go-nats".Conn) as *"github.com/nats-io/nats.go".Conn value in argument to stan.NatsConn

flaky tests because the stopped transport is reused in vicetest/test.go

This function

func Transport(t *testing.T, transport vice.Transport) {
	testStandardTransportBehaviour(t, transport)
	testSendChannelsDontBlock(t, transport)
}

uses the same transport for both tests.The first test stops the transport at the end, which is followed by some closed channels.

I get some flaky tests in the nats client code:

	go func() {
		for {
			select {
			case <-t.stopPubChan:
				return
			case msg := <-ch:
				if err := c.Publish(name, msg); err != nil {
					t.errChan <- vice.Err{Message: msg, Name: name, Err: err}
				}
			default:
			}
		}
	}()
=== RUN   TestTransport
        test.go:38: send channels shouldn't block

That is because the t.stopPubChan is already closed.

Maybe we should export these 2 functions

	testStandardTransportBehaviour(t, transport)
	testSendChannelsDontBlock(t, transport)

so that every queue can run these tests with a new transport.

Have you considered channels as call arguments rather than return arguments

Firstly, this is a lovely library. Love the test harness for implementers. I can see this helping people get started with queueing technologies really quickly.

Just a quick thought. Have you considered modeling the channel based functions in your interface as call arguments, rather that return arguments? I see this a lot in good channel practice.

For example:

// Transport provides message sending and receiving
// capabilities over a messaging queue technology.
// Clients should always check for errors coming through ErrChan.
type Transport interface {
	// Receive gets a channel on which to receive messages
	// with the specified name.
	Receive(name string, chan<- []byte) 
	// Send gets a channel on which messages with the
	// specified name may be sent.
	Send(name string, <-chan []byte)
	// ErrChan gets a channel through which errors
	// are sent.
	ErrChan(chan<- error)

	// Stop stops the transport. StopChan will be closed
	// when the transport has stopped.
	Stop()
	// Done gets a channel which is closed when the
	// transport has successfully stopped.
	Done() chan struct{}
}

Note that we have to flip the directions around on the channels. I think it could address some limitations of control for the consumer. For example, security that the implementation won't close the thing it is sending on or control over the channels buffer size.

For receive operations now you can expect the Receive function to potentially send or close, but not consume from your channel. That is purely the callers responsibility and they can set the buffer size.

For the Send operation, the consumer of this library is in control of when to close. The current implementation gives no guarantees to the user that the implementation of vice.Transport won't close the channel it is sending on, which would cause a panic. Enforcing that the implementation takes a receive only channel, means the transport can't close it, only stop consuming it.

Another recommendation I would have would be let Receive and Send take a context.Context and remove the Done() chan struct{} in favor of the Done on context.Context.

Otherwise, love the idea and might take a crack at a queue implementation πŸ‘

why not to reuse nats connection in makePublisher or makeSubscriber

func (t *Transport) newConnection() (*nats.Conn, error) {
	return nats.Connect(t.NatsAddr)
}
func (t *Transport) makeSubscriber(name string) (chan []byte, error) {
	ch := make(chan []byte)

	c, err := t.newConnection()
	if err != nil {
		return nil, err
	}

func (t *Transport) makePublisher(name string) (chan []byte, error) {
	ch := make(chan []byte)
	c, err := t.newConnection()
	if err != nil {
		return nil, err
	}

context

I toyed with the idea of adding context for cancellation, but it meant that the idea wasn't explicitly represented in the interface. I'd imagine the context would go into the New functions in the implementations, rather than Send and Receive? That's not a deal breaker, but that's why I left Stop and Done in there. But since context is in the std lib, it's definitely still on the table.

We could have a context passed in to the New functions of the transport implementations to handle stopping, instead of a Stop function.

I think we'd still want Done() because it's important that the implementations are given enough time to properly de-register their interest in the underlying queue. Or is there a more elegant way to do that?

Question about processing file between multiple nodes

Hi, I have a huge file, on each line I have a value to send for a GET request.

I would like to create a queue with the file information, from a master server.
and to be able to connect different nodes so that they take from the queue of messages, the values ​​to make the GET request and save the answer.

The nodes should not take the same message to process. double work should be avoided. If a node can not make the request, it could include the message in the queue again.

Can your library be useful to me? I am new programming in go lang.
Can you give me some recommendation?

Regards

Async Behaviour varies between nsq and Nats

nsq

  • if i kill the service, send a few messages from the client, and then restart the service, the client gets the messages back that were stored inside nsqd

nats

  • if i do the same, nats does not. The messages that were sent by the client are lost.

BTW since the API is meant to be agnostic to the underlying MQ, then can we use a string based Service factory i wonder :) I know its not proper golang, but well its a neat trick and will make testing / benching easier one might assume.

Redis queue imports fail - gopkg.in is not supported any longer

The redis Transport is currently failing:

vendor/gopkg.in/redis.v3/cluster.go:17:2: use of internal package github.com/xx/yy/vendor/github.com/go-redis/redis/internal not allowed vendor/gopkg.in/redis.v3/ring.go:14:2: use of internal package github.com/xx/yy/vendor/github.com/go-redis/redis/internal/consistenthash not allowed vendor/gopkg.in/redis.v3/cluster.go:18:2: use of internal package github.com/xx/yy/vendor/github.com/go-redis/redis/internal/hashtag not allowed vendor/gopkg.in/redis.v3/cluster.go:19:2: use of internal package github.com/xx/yy/vendor/github.com/go-redis/redis/internal/pool not allowed vendor/gopkg.in/redis.v3/cluster.go:20:2: use of internal package github.com/xx/yy/vendor/github.com/go-redis/redis/internal/proto not allowed vendor/gopkg.in/redis.v3/cluster.go:21:2: use of internal package github.com/xx/yy/vendor/github.com/go-redis/redis/internal/singleflight not allowed

This appears to be related to redis/go-redis#665

Updating the imports to github.com/go-redis/redis corrected the issue for me. I'll submit a PR shortly.

Nats queue Race Conditions

If i run the tests with the race detector enabled then i get:

==================
WARNING: DATA RACE
Read at 0x00c4200b4198 by goroutine 6:
  github.com/matryer/vice/queues/nats.(*Transport).makePublisher()
      /home/rkaufmann/go/src/github.com/matryer/vice/queues/nats/nats.go:140 +0xfe
  github.com/matryer/vice/queues/nats.(*Transport).Send()
      /home/rkaufmann/go/src/github.com/matryer/vice/queues/nats/nats.go:111 +0x161
  github.com/matryer/vice/vicetest.testStandardTransportBehaviour()
      /home/rkaufmann/go/src/github.com/matryer/vice/vicetest/test.go:97 +0x396
  github.com/matryer/vice/vicetest.Transport()
      /home/rkaufmann/go/src/github.com/matryer/vice/vicetest/test.go:26 +0x4c
  github.com/matryer/vice/queues/nats.TestTransport()
      /home/rkaufmann/go/src/github.com/matryer/vice/queues/nats/nats_test.go:16 +0x341
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:747 +0x16c

Previous write at 0x00c4200b4198 by goroutine 7:
  github.com/matryer/vice/queues/nats.(*Transport).makeSubscriber()
      /home/rkaufmann/go/src/github.com/matryer/vice/queues/nats/nats.go:94 +0x204
  github.com/matryer/vice/queues/nats.(*Transport).Receive()
      /home/rkaufmann/go/src/github.com/matryer/vice/queues/nats/nats.go:69 +0x15b
  github.com/matryer/vice/vicetest.testStandardTransportBehaviour.func2()
      /home/rkaufmann/go/src/github.com/matryer/vice/vicetest/test.go:83 +0x2e4

Goroutine 6 (running) created at:
  testing.(*T).Run()
      /usr/local/go/src/testing/testing.go:790 +0x568
  testing.runTests.func1()
      /usr/local/go/src/testing/testing.go:1005 +0xa7
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:747 +0x16c
  testing.runTests()
      /usr/local/go/src/testing/testing.go:1003 +0x521
  testing.(*M).Run()
      /usr/local/go/src/testing/testing.go:922 +0x206
  main.main()
      github.com/matryer/vice/queues/nats/_test/_testmain.go:48 +0x1d3

Goroutine 7 (running) created at:
  github.com/matryer/vice/vicetest.testStandardTransportBehaviour()
      /home/rkaufmann/go/src/github.com/matryer/vice/vicetest/test.go:56 +0x22f
  github.com/matryer/vice/vicetest.Transport()
      /home/rkaufmann/go/src/github.com/matryer/vice/vicetest/test.go:26 +0x4c
  github.com/matryer/vice/queues/nats.TestTransport()
      /home/rkaufmann/go/src/github.com/matryer/vice/queues/nats/nats_test.go:16 +0x341
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:747 +0x16c
==================
==================
WARNING: DATA RACE
Read at 0x00c4200949b0 by goroutine 6:
  runtime.growslice()
      /usr/local/go/src/runtime/slice.go:82 +0x0
  github.com/matryer/vice/queues/nats.(*Transport).makePublisher()
      /home/rkaufmann/go/src/github.com/matryer/vice/queues/nats/nats.go:140 +0x262
  github.com/matryer/vice/queues/nats.(*Transport).Send()
      /home/rkaufmann/go/src/github.com/matryer/vice/queues/nats/nats.go:111 +0x161
  github.com/matryer/vice/vicetest.testStandardTransportBehaviour()
      /home/rkaufmann/go/src/github.com/matryer/vice/vicetest/test.go:98 +0x40a
  github.com/matryer/vice/vicetest.Transport()
      /home/rkaufmann/go/src/github.com/matryer/vice/vicetest/test.go:26 +0x4c
  github.com/matryer/vice/queues/nats.TestTransport()
      /home/rkaufmann/go/src/github.com/matryer/vice/queues/nats/nats_test.go:16 +0x341
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:747 +0x16c

Previous write at 0x00c4200949b0 by goroutine 7:
  github.com/matryer/vice/queues/nats.(*Transport).makeSubscriber()
      /home/rkaufmann/go/src/github.com/matryer/vice/queues/nats/nats.go:94 +0x1cc
  github.com/matryer/vice/queues/nats.(*Transport).Receive()
      /home/rkaufmann/go/src/github.com/matryer/vice/queues/nats/nats.go:69 +0x15b
  github.com/matryer/vice/vicetest.testStandardTransportBehaviour.func2()
      /home/rkaufmann/go/src/github.com/matryer/vice/vicetest/test.go:83 +0x2e4

Goroutine 6 (running) created at:
  testing.(*T).Run()
      /usr/local/go/src/testing/testing.go:790 +0x568
  testing.runTests.func1()
      /usr/local/go/src/testing/testing.go:1005 +0xa7
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:747 +0x16c
  testing.runTests()
      /usr/local/go/src/testing/testing.go:1003 +0x521
  testing.(*M).Run()
      /usr/local/go/src/testing/testing.go:922 +0x206
  main.main()
      github.com/matryer/vice/queues/nats/_test/_testmain.go:48 +0x1d3

Goroutine 7 (running) created at:
  github.com/matryer/vice/vicetest.testStandardTransportBehaviour()
      /home/rkaufmann/go/src/github.com/matryer/vice/vicetest/test.go:56 +0x22f
  github.com/matryer/vice/vicetest.Transport()
      /home/rkaufmann/go/src/github.com/matryer/vice/vicetest/test.go:26 +0x4c
  github.com/matryer/vice/queues/nats.TestTransport()
      /home/rkaufmann/go/src/github.com/matryer/vice/queues/nats/nats_test.go:16 +0x341
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:747 +0x16c
==================
==================
WARNING: DATA RACE
Read at 0x00c420134208 by goroutine 82:
  github.com/matryer/vice/queues/nats.(*Transport).makePublisher()
      /home/rkaufmann/go/src/github.com/matryer/vice/queues/nats/nats.go:140 +0xfe
  github.com/matryer/vice/queues/nats.(*Transport).Send()
      /home/rkaufmann/go/src/github.com/matryer/vice/queues/nats/nats.go:111 +0x161
  github.com/matryer/vice/queues/nats.TestSend()
      /home/rkaufmann/go/src/github.com/matryer/vice/queues/nats/nats_test.go:88 +0x486
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:747 +0x16c

Previous write at 0x00c420134208 by goroutine 10:
  github.com/matryer/vice/queues/nats.(*Transport).makeSubscriber()
      /home/rkaufmann/go/src/github.com/matryer/vice/queues/nats/nats.go:94 +0x204
  github.com/matryer/vice/queues/nats.(*Transport).Receive()
      /home/rkaufmann/go/src/github.com/matryer/vice/queues/nats/nats.go:69 +0x15b
  github.com/matryer/vice/queues/nats.TestSend.func1()
      /home/rkaufmann/go/src/github.com/matryer/vice/queues/nats/nats_test.go:74 +0xbf

Goroutine 82 (running) created at:
  testing.(*T).Run()
      /usr/local/go/src/testing/testing.go:790 +0x568
  testing.runTests.func1()
      /usr/local/go/src/testing/testing.go:1005 +0xa7
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:747 +0x16c
  testing.runTests()
      /usr/local/go/src/testing/testing.go:1003 +0x521
  testing.(*M).Run()
      /usr/local/go/src/testing/testing.go:922 +0x206
  main.main()
      github.com/matryer/vice/queues/nats/_test/_testmain.go:48 +0x1d3

Goroutine 10 (running) created at:
  github.com/matryer/vice/queues/nats.TestSend()
      /home/rkaufmann/go/src/github.com/matryer/vice/queues/nats/nats_test.go:67 +0x43c
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:747 +0x16c
==================
==================
WARNING: DATA RACE
Read at 0x00c4200f40d0 by goroutine 82:
  runtime.growslice()
      /usr/local/go/src/runtime/slice.go:82 +0x0
  github.com/matryer/vice/queues/nats.(*Transport).makePublisher()
      /home/rkaufmann/go/src/github.com/matryer/vice/queues/nats/nats.go:140 +0x262
  github.com/matryer/vice/queues/nats.(*Transport).Send()
      /home/rkaufmann/go/src/github.com/matryer/vice/queues/nats/nats.go:111 +0x161
  github.com/matryer/vice/queues/nats.TestSend()
      /home/rkaufmann/go/src/github.com/matryer/vice/queues/nats/nats_test.go:88 +0x486
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:747 +0x16c

Previous write at 0x00c4200f40d0 by goroutine 10:
  github.com/matryer/vice/queues/nats.(*Transport).makeSubscriber()
      /home/rkaufmann/go/src/github.com/matryer/vice/queues/nats/nats.go:94 +0x1cc
  github.com/matryer/vice/queues/nats.(*Transport).Receive()
      /home/rkaufmann/go/src/github.com/matryer/vice/queues/nats/nats.go:69 +0x15b
  github.com/matryer/vice/queues/nats.TestSend.func1()
      /home/rkaufmann/go/src/github.com/matryer/vice/queues/nats/nats_test.go:74 +0xbf

Goroutine 82 (running) created at:
  testing.(*T).Run()
      /usr/local/go/src/testing/testing.go:790 +0x568
  testing.runTests.func1()
      /usr/local/go/src/testing/testing.go:1005 +0xa7
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:747 +0x16c
  testing.runTests()
      /usr/local/go/src/testing/testing.go:1003 +0x521
  testing.(*M).Run()
      /usr/local/go/src/testing/testing.go:922 +0x206
  main.main()
      github.com/matryer/vice/queues/nats/_test/_testmain.go:48 +0x1d3

Goroutine 10 (running) created at:
  github.com/matryer/vice/queues/nats.TestSend()
      /home/rkaufmann/go/src/github.com/matryer/vice/queues/nats/nats_test.go:67 +0x43c
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:747 +0x16c
==================

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.