Code Monkey home page Code Monkey logo

dque's Introduction

dque - a fast embedded durable queue for Go

Go Report Card GoDoc

dque is:

  • persistent -- survives program restarts
  • scalable -- not limited by your RAM, but by your disk space
  • FIFO -- First In First Out
  • embedded -- compiled into your Golang program
  • synchronized -- safe for concurrent usage
  • fast or safe, you choose -- turbo mode lets the OS decide when to write to disk
  • has a liberal license -- allows any use, commercial or personal

I love tools that do one thing well. Hopefully this fits that category.

I am indebted to Gabor Cselle who, years ago, inspired me with an example of an in-memory persistent queue written in Java. I was intrigued by the simplicity of his approach, which became the foundation of the "segment" part of this queue which holds the head and the tail of the queue in memory as well as storing the segment files in between.

performance

There are two performance modes: safe and turbo

safe mode
  • safe mode is the default
  • forces an fsync to disk every time you enqueue or dequeue an item.
  • while this is the safest way to use dque with little risk of data loss, it is also the slowest.
turbo mode
  • can be enabled/disabled with a call to DQue.TurboOn() or DQue.TurboOff()
  • lets the OS batch up your changes to disk, which makes it a lot faster.
  • also allows you to flush changes to disk at opportune times. See DQue.TurboSync()
  • comes with a risk that a power failure could lose changes. By turning on Turbo mode you accept that risk.
  • run the benchmark to see the difference on your hardware.
  • there is a todo item to force flush changes to disk after a configurable amount of time to limit risk.

implementation

  • The queue is held in segments of a configurable size.
  • The queue is protected against re-opening from other processes.
  • Each in-memory segment corresponds with a file on disk. Think of the segment files as a bit like rolling log files. The oldest segment files are eventually deleted, not based on time, but whenever their items have all been dequeued.
  • Segment files are only appended to until they fill up. At which point a new segment is created. They are never modified (other than being appended to and deleted when each of their items has been dequeued).
  • If there is more than one segment, new items are enqueued to the last segment while dequeued items are taken from the first segment.
  • Because the encoding/gob package is used to store the struct to disk:
    • Only structs can be stored in the queue.
    • Only one type of struct can be stored in each queue.
    • Only public fields in a struct will be stored.
    • A function is required that returns a pointer to a new struct of the type stored in the queue. This function is used when loading segments into memory from disk. I'd love to find a way to avoid this function.
  • Queue segment implementation:
    • For nice visuals, see Gabor Cselle's documentation here. Note that Gabor's implementation kept the entire queue in memory as well as disk. dque keeps only the head and tail segments in memory.
    • Enqueueing an item adds it both to the end of the last segment file and to the in-memory item slice for that segment.
    • When a segment reaches its maximum size a new segment is created.
    • Dequeueing an item removes it from the beginning of the in-memory slice and appends a 4-byte "delete" marker to the end of the segment file. This allows the item to be left in the file until the number of delete markers matches the number of items, at which point the entire file is deleted.
    • When a segment is reconstituted from disk, each "delete" marker found in the file causes a removal of the first element of the in-memory slice.
    • When each item in the segment has been dequeued, the segment file is deleted and the next segment is loaded into memory.

example

See the full example code here

Or a shortened version here:

package dque_test

import (
    "log"

    "github.com/joncrlsn/dque"
)

// Item is what we'll be storing in the queue.  It can be any struct
// as long as the fields you want stored are public.
type Item struct {
    Name string
    Id   int
}

// ItemBuilder creates a new item and returns a pointer to it.
// This is used when we load a segment of the queue from disk.
func ItemBuilder() interface{} {
    return &Item{}
}

func main() {
    ExampleDQue_main()
}

// ExampleQueue_main() show how the queue works
func ExampleDQue_main() {
    qName := "item-queue"
    qDir := "/tmp"
    segmentSize := 50

    // Create a new queue with segment size of 50
    q, err := dque.New(qName, qDir, segmentSize, ItemBuilder)
    ...

    // Add an item to the queue
    err := q.Enqueue(&Item{"Joe", 1})
    ...

    // Properly close a queue
    q.Close()

    // You can reconsitute the queue from disk at any time
    q, err = dque.Open(qName, qDir, segmentSize, ItemBuilder)
    ...

    // Peek at the next item in the queue
    var iface interface{}
    if iface, err = q.Peek(); err != nil {
        if err != dque.ErrEmpty {
            log.Fatal("Error peeking at item ", err)
        }
    }

    // Dequeue the next item in the queue
    if iface, err = q.Dequeue(); err != nil {
        if err != dque.ErrEmpty {
            log.Fatal("Error dequeuing item ", err)
        }
    }

    // Dequeue the next item in the queue and block until one is available
    if iface, err = q.DequeueBlock(); err != nil {
        log.Fatal("Error dequeuing item ", err)
    }

    // Assert type of the response to an Item pointer so we can work with it
    item, ok := iface.(*Item)
    if !ok {
        log.Fatal("Dequeued object is not an Item pointer")
    }

    doSomething(item)
}

func doSomething(item *Item) {
    log.Println("Dequeued", item)
}

contributors

todo? (feel free to submit pull requests)

  • add option to enable turbo with a timeout that would ensure you would never lose more than n seconds of changes.
  • add Lock() and Unlock() methods so you can peek at the first item and then conditionally dequeue it without worrying that another goroutine has grabbed it out from under you. The use case is when you don't want to actually remove it from the queue until you know you were able to successfully handle it.
  • store the segment size in a config file inside the queue. Then it only needs to be specified on dque.New(...)

alternative tools

  • CurlyQ is a bit heavier (requires Redis) but has more background processing features.

dque's People

Contributors

damz avatar joncrlsn avatar kriechi avatar ms-meya avatar neilisaac 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

dque's Issues

DQue should be an Interface

Although the current state works great, think about changing DQue as the return of New() to an interface. This makes it much easier for testing purposes. A dummy implementation of the interface would avoid any write/lock implications during testing.

type DQue interface {
  Close() error
  Enqueue(obj interface{}) error
  Dequeue() (interface{}, error)
  Peek() (interface{}, error)
  DequeueBlock() (interface{}, error)
  PeekBlock() (interface{}, error)
  Size() int
  SizeUnsafe() int
  SegmentNumbers() (int, int)
  Turbo() bool
  TurboOn() error
  TurboOff() error
  TurboSync() error
}

Segment files are not closed explicitly

If the queue grows faster than it is consumed, Enqueue will append new segments. newQueueSegment opens segment files with os.O_APPEND|os.O_CREATE|os.O_WRONLY but they are only closed (a) if they become the firstSegment and get fully consumed and removed via delete in Dequeue, or (b) closed by the os.File finalizer. This may result in many open file descriptors until the garbage collector runs.

Batch peek and dequeue operations

For the sake of discussion, I'm considering adding batch uploads from a queue consumer, but would require a batch read API.

Suggested API:

// BatchPeek returns a slice of up to 1-n objects without dequeueing them.
// Fewer than n items may be returned, depending on the remaining objects in the first segment.
// ErrEmpty is returned if the queue is empty.
BatchPeek(n int) ([]interface{}, error)

// BatchDequeue dequeues and returns a slice of up to 1-n objects.
// Fewer than n items may be returned, depending on the remaining objects in the first segment.
// ErrEmpty is returned if the queue is empty.
BatchDequeue(n int) ([]interface{}, error)

We are still failing...

jcarlson
"If we still have to log into the servers in a year then it sounds like we'd be technically failing at devops.” at 9AM Monday, March 4th, 2019.

Also, hello!

Dequeue is failing while deleting the segment "The process cannot access the file because it is being used by another process"

Hi,
I was trying a simple program to enqueue and dequeue 10 elements. with segment size 5
its failing with below error
main.go and go.mod files attached
PS C:\Sai_laptop_backup\workspace\golang\dque_test> .\main.exe
Enqueued SAI100 1
Enqueued SAI101 2
Enqueued SAI102 3
Enqueued SAI103 4
Enqueued SAI104 5
Enqueued SAI105 6
Enqueued SAI106 7
Enqueued SAI107 8
Enqueued SAI108 9
Enqueued SAI109 10
Dequeued: &{SAI100 1}
Dequeued: &{SAI101 2}
Dequeued: &{SAI102 3}
Dequeued: &{SAI103 4}
2021/07/20 11:22:49 Error dequeuing item:error deleting queue segment C:/Sai_laptop_backup/workspace/golang/dque_test/item-queue/0000000000001.dque. Queue is in an inconsistent state: error deleting file: C:/Sai_laptop_backup/workspace/golang/dque_test/item-queue/0000000000001.dque: remove C:/Sai_laptop_backup/workspace/golang/dque_test/item-queue/0000000000001.dque: The process cannot access the file because it is being used by another process.

Occasional: Queue is in an inconsistent state

I've been trying to figure this out for a while but it's super hard to reproduce. Occasionally, I get the following error:

Caused by: error deleting queue segment /tmp/ripple/600f5b80f62ab2ff24e82005/hb-queue/0000000000001.dque. Queue 
is in an inconsistent state: error deleting file: /tmp/ripple/600f5b80f62ab2ff24e82005/hb-queue/0000000000001.dque: 
remove /tmp/ripple/600f5b80f62ab2ff24e82005/hb-queue/0000000000001.dque: no such file or directory

Are there any known causes for these inconsistencies that I might have missed? I'm not doing anything special except I am using the same instance of the queue in different goroutines. One does all the writing while the other one dequeues.

Sometimes I go weeks without seeing this and then it just pops up.

Question: single process?

This is intended to work within a single process, true? That is, could one push items into the queue from one process and read from the queue by one or more workers?

Unable to import v2.x.x in go.mod

Hi I get following error if I include following in my go.mod file.

github.com/joncrlsn/dque v2.2.0

The error I get is

#12 0.387 go: errors parsing go.mod:
#12 0.387 /edge/go.mod:11:2: require github.com/joncrlsn/dque: version "v2.2.0" invalid: should be v0 or v1, not v2

This is probably related to https://go.dev/blog/v2-go-modules. If that's true, can you please make the v2 code follow these guidlines.

Wrong marshaling

package event

import (
	"testing"

	"github.com/davecgh/go-spew/spew"
	"github.com/joncrlsn/dque"
)

func TestName(t *testing.T) {
	q, err := dque.NewOrOpen("q", ".", 2, func() interface{} {
		return new(string)
	})
	if err != nil {
		t.Fatal(err)
	}
	defer q.Close()

	if err := q.TurboOn(); err != nil {
		t.Fatal(err)
	}

	if err := q.Enqueue("1"); err != nil {
		t.Fatal(err)
	}
	if err := q.Enqueue("2"); err != nil {
		t.Fatal(err)
	}
	if err := q.Enqueue("3"); err != nil {
		t.Fatal(err)
	}
	if err := q.Enqueue("4"); err != nil {
		t.Fatal(err)
	}
	if err := q.Enqueue("5"); err != nil {
		t.Fatal(err)
	}

	for {
		item, err := q.Dequeue()
		if err == dque.ErrEmpty {
			return
		}
		if err != nil {
			t.Fatal(err)
		}
		spew.Dump(item)
	}
}

image

invalid memory address or nil pointer dereference on dequeue

Hey, just started using this library and I have run into an issue dequeuing a queue.

[signal SIGSEGV: segmentation violation code=0x1 addr=0x58 pc=0x181f8b1]

goroutine 10 [running]:
sync.(*Mutex).Lock(...)
	/usr/local/Cellar/go/1.13.6/libexec/src/sync/mutex.go:74
github.com/joncrlsn/dque.(*DQue).Dequeue(0x0, 0x0, 0x0, 0x0, 0x0)
	/Users/dlotz/go/pkg/mod/github.com/joncrlsn/[email protected]/queue.go:240 +0x51

I am not sure if you have any thoughts on the cause of this, my googling does not seem to turn anything up.  

Move item to end of queue

It would be awesome to have a way to move an item to the end of the queue instead of dequeue and enqueue again. My use case is the following: I use a queue to sequentially send requests and retry failed ones for a couple of times after a certain time threshold. I always peek (block) the first item and after it was processed, dequeue it. When it failed and didn't fail for more than 3 times, I enqueue it again.

Corrupted data when opening existing queue

I took an example_test.go and just run two goroutines one enqueueing consecutive integers, another doing blocking dequeue and just print them from time to time.

Segment size 50, then switches to 100000.

Interrupting the program, and the starting it again, causes it to read corrupted data:

2022/02/23 15:26:49 Error creating new dque unable to create queue segment in /tmp/item-queue: unable to load queue segment in /tmp/item-queue: segment file /tmp/item-queue/0000000000041.dque is corrupted: error reading gob data from file: EOF exit status 1

Source:

package main

import (
    "fmt"
    "log"

    "github.com/joncrlsn/dque"
)

func main() {
    ExampleDQue()
}

// Item is what we'll be storing in the queue.  It can be any struct
// as long as the fields you want stored are public.
type Item struct {
	Name string
	Id   int
}

// ItemBuilder creates a new item and returns a pointer to it.
// This is used when we load a segment of the queue from disk.
func ItemBuilder() interface{} {
	return &Item{}
}

// ExampleDQue shows how the queue works
func ExampleDQue() {
	qName := "item-queue"
	qDir := "/tmp"
	segmentSize := 100000

	q, err := dque.NewOrOpen(qName, qDir, segmentSize, ItemBuilder)
	if err != nil {
		log.Fatal("Error creating new dque ", err)
	}

	go func() {
		i := 0
		for {
			err := q.Enqueue(&Item{"Joe", i})
			if err != nil {
				log.Fatal("Error enqueueing", err)
			}

			i++
			//log.Println("Queue size:", q.Size())
		}
	}()

	func() {
		for {
			var iface interface{}

			// Dequeue the next item in the queue and block until one is available
			if iface, err = q.DequeueBlock(); err != nil {
				log.Fatal("Error dequeuing item ", err)
			}

			// Assert type of the response to an Item pointer so we can work with it
			item, ok := iface.(*Item)
			if !ok {
				log.Fatal("Dequeued object is not an Item pointer")
			}

			doSomething(item)
		}
	}()
}

func doSomething(item *Item) {
	if item.Id % 100000 == 0 {
		fmt.Println("Dequeued:", item)
	}
}

Cannot get latest version: module contains a go.mod file, so module path should be github.com/joncrlsn/dque/v2

Background

The github.com/joncrlsn/dque uses Go modules and the current release version is v2. And it’s module path is "github.com/joncrlsn/dque", instead of "github.com/joncrlsn/dque/v2". It must comply with the specification of "Releasing Modules for v2 or higher" available in the Modules documentation. Quoting the specification:

A package that has opted in to modules must include the major version in the import path to import any v2+ modules
To preserve import compatibility, the go command requires that modules with major version v2 or later use a module path with that major version as the final element. For example, version v2.0.0 of example.com/m must instead use module path example.com/m/v2.
https://github.com/golang/go/wiki/Modules#releasing-modules-v2-or-higher

Steps to Reproduce

GO111MODULE=on, run go get targeting any version > v2.0.0 of the joncrlsn/dque:

$ go get github.com/joncrlsn/[email protected]
go: finding github.com/joncrlsn/dque v2.2.0
go: finding github.com/joncrlsn/dque v2.2.0
go get github.com/joncrlsn/[email protected]: github.com/joncrlsn/[email protected]: invalid version: module contains a go.mod file, so major version must be compatible: should be v0 or v1, not v2

SO anyone using Go modules will not be able to easily use any newer version of joncrlsn/dque.

Solution

1. Kill the go.mod files, rolling back to GOPATH.

This would push them back to not being managed by Go modules (instead of incorrectly using Go modules).
Ensure compatibility for downstream module-aware projects and module-unaware projects projects

2. Fix module path to strictly follow SIV rules.

Patch the go.mod file to declare the module path as github.com/joncrlsn/dque/v2 as per the specs. And adjust all internal imports.
The downstream projects might be negatively affected in their building if they are module-unaware (Go versions older than 1.9.7 and 1.10.3; Or use third-party dependency management tools, such as: Dep, glide,govendor…).

[*] You can see who will be affected here: [3 module-unaware users, i.e., kidmam/golangstudy, dbit-xia/go_demo, hwahrmann/rsa-nw-syslog-receiver]
https://github.com/search?l=Go&q=joncrlsn%2Fdque&type=Code

If you don't want to break the above repos. This method can provides better backwards-compatibility.
Release a v2 or higher module through the major subdirectory strategy: Create a new v2 subdirectory (github.com/joncrlsn/dque/v2) and place a new go.mod file in that subdirectory. The module path must end with /v2. Copy or move the code into the v2 subdirectory. Update import statements within the module to also use /v2 (import "github.com/joncrlsn/dque/v2/…"). Tag the release with v2.x.y.

3. Suggest your downstream module users use hash instead of a version tag.

If the standard rule of go modules conflicts with your development mode. Or not intended to be used as a library and does not make any guarantees about the API. So you can’t comply with the specification of "Releasing Modules for v2 or higher" available in the Modules documentation.
Regardless, since it's against one of the design choices of Go, it'll be a bit of a hack. Instead of go get github.com/joncrlsn/dque@version-tag, module users need to use this following way to get the joncrlsn/dque:
(1) Search for the tag you want (in browser)
(2) Get the commit hash for the tag you want
(3) Run go get github.com/joncrlsn/dque@commit-hash
(4) Edit the go.mod file to put a comment about which version you actually used
This will make it difficult for module users to get and upgrade joncrlsn/dque.

[*] You can see who will be affected here: [7 module users, e.g., grafana/loki, ksensehq/eventnative, getoctane/octane-collector]
https://github.com/search?q=%22github.com%2Fjoncrlsn%2Fdque%22+filename%3Ago.mod&type=Code

Summary

You can make a choice to fix DM issues by balancing your own development schedules/mode against the affects on the downstream projects.

For this issue, Solution 2 can maximize your benefits and with minimal impacts to your downstream projects the ecosystem.

References

Run unit tests and lint checks in CI

I recommend running https://github.com/golangci/golangci-lint on all projects since it has a good default set of lint checks which frequently uncover real issues.

Travis and Github Actions appear to have the best support for matrix builds (to run the tests on multiple go versions) so I'd recommend one of those. I'm interested in trying Actions. I also have a lot of experience using CircleCI but would mainly recommend it for teams with multiple repositories. Any preference?

DQue.Size and SegmentNumbers are not thread-safe

First off, thanks for writing this package! I'm doing a brief code review on it to evaluate it for production use, and am filing issues along the way. Please let me know if you'd rather we fork it or submit patches for review.

qSegment.size() is thread-safe, however DQue.Size is not since firstSegment and lastSegment may be updated asynchronously (ex. from Enqueue/Dequeue).

releases the lock when load fail

for some reason, load() may fail when we init dque. the lock should be releases that we could reinitialize dque

if err := q.lock(); err != nil {
return nil, err
}
if err := q.load(); err != nil {
return nil, err
}

Implement lazy decoding

Dequeue has variable latency since it may advance firstSegment, resulting in all elements of the next file getting decoded synchronously. This will hold the mutex for an extended period of time, blocking Enqueue operations, and may delay the consumer unnecessarily.

Instead of Peek() (interface{}, error) and Dequeue() (interface{}, error) we could have

Peek(interface{}) error
Dequeue(interface{}) error

This emulates the API from json.Decoder.Decode, removing the need for providing an object builder.

This would allow storing []byte arrays for each object rather than decoded objects. This may also reduce memory due to gob's encoding format, depending on the application.

A further optimization to consider is seeking within the file, rather than loading the whole file into memory.

This is not critical for my current application, but is worth discussing/considering.

What could cause a decode failure on the segmentSize boundary?

I am trying a consumer/producer program and it reliably fails on the nth item, (where n is the segmentSize)
I can dequeue n-1 records with no issue, and the 0000000000001.dque file disappears as expected, but that nth item always gives me this error.
Both the producer and consumer are separate coroutines.
Any idea before I try to make a simple example that reproduces the error?

error creating new segment. Queue is in an inconsistent state: unable to load queue segment in /tmp/receiver_a_1_queue_28762: object in segment file /tmp/rmyqueue/0000000000002.dque cannot be decoded: failed to decode *main.Item: gob: type mismatch: no fields matched compiling decoder for Item

Item is

type RequestRec struct {
	Topic string
	Body  string
	Count int // how many time we have resent that request
}

Blocking Dequeue & Peek functions

The existing Dequeue() and Peek() functions simply return an ErrEmpty error if no items are in the queue.

I found myself trying to replace a go channel with dque, but was missing a proper blocking Dequeue function that only returns once at least one item is available.

Any thoughts on accepting such a feature request / PR?

Windows & leaking filedescriptors

This doesn't seem to work at all on windows. Very few tests pass. This is mostly due to not closing segment files properly.

--- FAIL: TestSegment (0.02s)
        segment_test.go:82: Error cleaning up directory from the TestSegment method with 'remove ./TestSegment\0000000000001.dque: The process cannot access the file because it is being used by another process.'
--- FAIL: TestSegment_Turbo (0.00s)
        segment_test.go:111: Error creating directory in the TestSegment_Turbo method: mkdir ./TestSegment: Cannot create a file when that file already exists.
--- FAIL: TestQueue_AddRemoveLoop (0.01s)
        queue_test.go:75: Error cleaning up the queue directory remove test1\0000000000002.dque: The process cannot access the file because it is being used by another process.
--- FAIL: TestQueue_Add2Remove1 (0.00s)
        queue_test.go:88: Error removing queue directory remove test1\0000000000002.dque: The process cannot access the file because it is being used by another process.
--- FAIL: TestQueue_Add9Remove8 (0.00s)
        queue_test.go:154: Error removing queue directory remove test1\0000000000002.dque: The process cannot access the file because it is being used by another process.
--- FAIL: TestQueue_EmptyDequeue (0.00s)
        queue_test.go:240: Error cleaning up the queue directory: remove testEmptyDequeue\0000000000001.dque: The process cannot access the file because it is being used by another process.
--- FAIL: TestQueue_NewOrOpen (0.00s)
        queue_test.go:262: Error cleaning up the queue directory: remove testNewOrOpen\0000000000001.dque: The process cannot access the file because it is being used by another process.
--- FAIL: TestQueue_Turbo (0.00s)
        queue_test.go:269: Error removing queue directory: remove testNewOrOpen\0000000000001.dque: The process cannot access the file because it is being used by another process.
2019/01/24 14:38:32 Error creating new dque the given queue directory is not valid (/tmp)
exit status 1
FAIL    github.com/joncrlsn/dque        0.103s

The tests all seem to pass on nix but i'm guessing it is leaking filedescriptors there aswell.

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.