• Stars
    star
    154
  • Rank 233,618 (Top 5 %)
  • Language
    TypeScript
  • License
    MIT License
  • Created over 1 year ago
  • Updated 10 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

JavaScript utilities for working with LiteFS on Fly.io

litefs-js 🎈

JavaScript utilities for working with LiteFS on Fly.io


Build Status version MIT License

The problem

Deploying your app to multiple regions along with your data is a great way to make your app really fast, but there are two issues:

  1. Read replica instances can only read from the database, they cannot write to it.
  2. There's an edge case where the user could write to the primary instance and then read from a replica instance before replication is finished.

The first problem is as simple as making sure you use a special fly-replay response so Fly can pass the request to the primary instance:

a visualization of the user making a request which is sent to a read replica and replayed to the primary instance

But the second problem is a little harder. Here's how we visualize that:

continuing the previous visualization with the edge case that the read replica responds to a get request before the replication has finished

This solution

This module comes with several utilities to help you work around these issues. Specifically, it allows you an easy way to add a special cookie to the client that identifies the client's "transaction number" which is then used by read replicas to compare to their local transaction number and force the client to wait until replication has finished if necessary (with a timeout).

Here's how we visualize that:

a visualization that shows the primary server sending a transaction number to the client and then the subsequent get request is sent to the replica which waits for replication to finish before responding

The even better (experimental) proxy solution

At the time of this writing, LiteFS just released experimental support for a proxy server that will handle much of this stuff for you. You simply configure the proxy server in your litefs.yml and then you don't need to bother with the tx number cookie or ensuring primary on non-get requests at all. The litefs-js module is still useful for one-off situations where you're making mutations in GET requests for example, or if you need to know more about the running instances of your application, but for most of the use cases, you can get away with using the proxy. Learn more about using the proxy from this PR.

Installation

This module is distributed via npm which is bundled with node and should be installed as one of your project's dependencies:

npm install --save litefs-js

Unless you plan on using lower-level utilities, you'll need to set two environment variables on your server:

  • LITEFS_DIR - the directory where the .primary file is stored. This should be what you set your fuse.dir config to in the litefs.yml config.
  • DATABASE_FILENAME - the filename of your sqlite database. This is used to determine the location of the -pos file which LiteFS uses to track the transaction number.
  • INTERNAL_PORT - the port set in the fly.toml (can be different from PORT if you're using the litefs proxy). This is useful for the getInternalInstanceDomain utility.

Usage

The best way to use this is with the latest version of LiteFS which supports the proxy server:

# litefs.yml
# ...
proxy:
  # matches the internal_port in fly.toml
  addr: ':${INTERNAL_PORT}'
  target: 'localhost:${PORT}'
  db: '${DATABASE_FILENAME}'
# ...

From there all you really need litefs-js for is when you run your database migrations. For example:

// start.js
const fs = require('fs')
const { spawn } = require('child_process')
const os = require('os')
const path = require('path')
const { getInstanceInfo } = require('litefs-js')

async function go() {
	const { currentInstance, currentIsPrimary, primaryInstance } =
		await getInstanceInfo()

	if (currentIsPrimary) {
		console.log(
			`Instance (${currentInstance}) in ${process.env.FLY_REGION} is primary. Deploying migrations.`,
		)
		await exec('npx prisma migrate deploy')
	} else {
		console.log(
			`Instance (${currentInstance}) in ${process.env.FLY_REGION} is not primary (the primary instance is ${primaryInstance}). Skipping migrations.`,
		)
	}

	console.log('Starting app...')
	await exec('node ./build')
}
go()

async function exec(command) {
	const child = spawn(command, { shell: true, stdio: 'inherit' })
	await new Promise((res, rej) => {
		child.on('exit', code => {
			if (code === 0) {
				res()
			} else {
				rej()
			}
		})
	})
}

The only other thing you need to worry about is if you ever perform a mutation as a part of a GET request. The proxy will handle forwarding non-GET requests for you automatically, but if you need to write to a non-primary in a GET, you'll need to use some of the utilities to ensure the server replays the request to the primary.

Lower Level Usage

Integrating this with your existing server requires integration in two places:

  1. Setting the transaction number cookie on the client after mutations have finished
  2. Waiting for replication to finish before responding to requests

Low-level utilities are exposed, but higher level utilities are also available for express and remix.

Additionally, any routes that trigger database mutations will need to ensure they are running on the primary instance, which is where ensurePrimary comes in handy.

Express

import express from 'express'
import {
	getSetTxNumberMiddleware,
	getTransactionalConsistencyMiddleware,
	getEnsurePrimaryMiddleware,
} from 'litefs-js/express'

const app = express()
// this should appear before any middleware that mutates the database
app.use(getEnsurePrimaryMiddleware())

// this should appear before any middleware that retrieves something from the database
app.use(getTransactionalConsistencyMiddleware())

// ... other middleware that might mutate the database here
app.use(getSetTxNumberMiddleware())

// ... middleware that send the response here

The tricky bit here is that often your middleware that mutates the database is also responsible for sending the responses, so you may need to use a lower-level utility like setTxCookie to set the cookie after mutations.

Remix

Until we have proper middleware support in Remix, you'll have to use the express or other lower-level utilities. You cannot currently use this module with the built-in Remix server because there's no way to force the server to wait before calling your loaders. Normally, you just need to use getTransactionalConsistencyMiddleware in express, and then you can use appendTxNumberCookie as shown below.

Of course, instead of using express with getTransactionalConsistencyMiddleware, you could use await handleTransactionalConsistency(request) to the top of every loader if you like:

// app/root.tsx (and app/routes/*.tsx... and every other loader in your app)
export function loader({ request }: DataFunctionArgs) {
	await handleTransactionalConsistency(request)
	// ... your loader code here
}

The same thing applies to getEnsurePrimaryMiddleware as well. If you need or like, you can use await ensurePrimary() in every action call or any loaders that mutate the database (of which, there should be few because you should avoid mutations in loaders).

We're umm... really looking forward to Remix middleware...

The appendTxNumberCookie utility should be used in the entry.server.ts file in both the default export (normally people call this handleDocumentRequest or handleRequest) and the handleDataRequest export.

// app/entry.server.ts
import { appendTxNumberCookie } from 'litefs-js/remix'

export default async function handleRequest(
	request: Request,
	responseStatusCode: number,
	responseHeaders: Headers,
	remixContext: EntryContext,
) {
	// Most of the time, all mutations are finished by now, but just make sure
	// you're finished with all mutations before this line:
	await appendTxNumberCookie(request, responseHeaders)
	// send the response
}

export async function handleDataRequest(
	response: Response,
	{ request }: Parameters<HandleDataRequestFunction>[1],
) {
	// Most of the time, all mutations are finished by now, but just make sure
	// you're finished with all mutations before this line:
	await appendTxNumberCookie(request, response.headers)
	return response
}

Other

There are several other lower-level utilities that you can use. They allow for more customization and are documented via jsdoc. Utilities you may find helpful:

  • ensurePrimary - Use this to ensure that the server that's handling the request is the primary server. This is useful if you know you need to do a mutation for that request.
  • getInstanceInfo - get the currentInstance and primaryInstance hostnames from the filesystem.
  • waitForUpToDateTxNumber - wait for the local transaction number to match the one you give it
  • getTxNumber - read the transaction number from the filesystem.
  • getTxSetCookieHeader - get the Set-Cookie header value for the transaction number
  • checkCookieForTransactionalConsistency - the logic used to check the transaction number cookie for consistency and wait for replication if necessary.
  • getAllInstances - get all the instances of your app currently running
  • getInternalInstanceDomain - get the internal domain for the current instance so you can communicate between instances of your app (ensure you've set the INTERNAL_PORT environment variable to what appears in your fly.toml).

How it works

This module uses the special .primary directory in your Fuse filesystem to determine the primary (litefs primary docs), and the -pos file to determine the transaction number (litefs transaction number docs).

When necessary, replay requests are made by responding with a 409 status code and a fly-replay header (docs on dynamic request routing).

Inspiration

This was built to make it much easier for people to take advantage of distributed SQLite with LiteFS on Fly.io. The bulk of the logic was extracted from kentcdodds/kentcdodds.com.

LICENSE

MIT

More Repositories

1

dockerfile-rails

Provides a Rails generator to produce Dockerfiles and related files.
Dockerfile
422
star
2

postgres-ha

Postgres + Stolon for HA clusters as Fly apps.
Go
287
star
3

nginx-cluster

A horizontally scalable NGINX caching cluster
Shell
125
star
4

terraform-provider-fly

Terraform provider for the Fly.io API
Go
114
star
5

dockerfile-node

Dockerfile generator for Node.js
JavaScript
111
star
6

edge-apollo-cache

Run and cache results from your Apollo GraphQL server on the edge with Fly
JavaScript
91
star
7

redis-geo-cache

A global Redis cache
Shell
81
star
8

bun

Bun JS app doing basically nothing
TypeScript
76
star
9

redis

Launch a Redis server on Fly
Shell
70
star
10

fly-run-this-function-on-another-machine

This is a simple example on how to spawn a Fly.io machine and run a function from there.
JavaScript
63
star
11

hello-rust

Rust example app on Fly.io
Dockerfile
62
star
12

nats-cluster

Global messaging for apps that need to talk to each other.
Go
44
star
13

postgres-flex

Postgres HA setup using repmgr
Go
44
star
14

tailscale-router

Go
40
star
15

rds-connector

Trivial Terraform example for a WireGuard peer to RDS
HCL
38
star
16

docker-daemon

A Docker daemon to run in Fly and access via a WireGuard peer.
Shell
38
star
17

fly-laravel

Run your Laravel apps on Fly
PHP
38
star
18

hello_elixir

An example for building and deploying an Elixir application to Fly using a Dockerfile
Elixir
38
star
19

litestream-base

A base Docker image for adding Litestream to apps
Dockerfile
33
star
20

smokescreen

An example of deploying Smokescreen on Fly.io
Go
31
star
21

go-example

A minimal Go application for tutorials
Go
29
star
22

python-hellofly-flask

A Pythonic version of the Hellofly example
Python
29
star
23

laravel-docker

Base Docker images for use with Laravel on Fly.io
Shell
28
star
24

cockroachdb

Shell
27
star
25

nginx

A fly app nginx config
Dockerfile
23
star
26

supercronic

Run periodic jobs on Fly with supercronic
Dockerfile
21
star
27

hello-fly-langchain

A minimal example of how to deploy LangChain to Fly.io using Flask
Python
21
star
28

hellonode-builtin

A minimal Fly example Node application for use in tutorials
JavaScript
20
star
29

vscode-remote

Shell
19
star
30

privatenet

Examples around querying 6PN private networking on Fly
JavaScript
18
star
31

node-demo

Fly.io Node.js demo
JavaScript
18
star
32

puppeteer-js-renderer

A service to render js for web scraping hosted on fly.io
JavaScript
17
star
33

hello-static

Create a static website with Fly - HTML from the example
HTML
16
star
34

ghost-litestream

Ghost + Litestream for global sqlite blogging
Dockerfile
16
star
35

wordpress-sqlite

Wordpress on SQLite
PHP
16
star
36

coredns

Authoritative CoreDNS on Fly.io
DIGITAL Command Language
15
star
37

ichabod

serf + headless chromium && CDP
Dockerfile
14
star
38

nix-base

Nix overlays for supporting Nix deployments on Fly.io
Nix
14
star
39

fly-log-local

Store Fly app logs locally.
Dockerfile
13
star
40

terraformed-machines

Example of Fly.io machines orchestration with Terraform and DNSimple
HCL
13
star
41

elixir_opentel_and_grafana

Project that goes with a Fly.io Phoenix Files article
Elixir
13
star
42

dockerfile-laravel

PHP
13
star
43

fastify-functions

Example Fastify server
JavaScript
12
star
44

postgres-migrator

Fly app that works to streamline Postgres migrations.
Dockerfile
12
star
45

pdf-appliance

Auto start machines that will generate PDFs for your application
TypeScript
12
star
46

hellodeno

A version of the Hellodeno example that uses flyctl's builtin deno builder
TypeScript
11
star
47

whisper-example

Fly GPU Machines transcribing an mp3 file with Whisper
Dockerfile
11
star
48

hello_elixir_sqlite

An example for building and deploying an Elixir application to Fly using a Dockerfile and SQLite!
Elixir
10
star
49

keydb

KeyDB server on Fly
Shell
9
star
50

fly-app-with-multiple-internal-ports

Example of how to deploy an app that has multiple ports listened to
JavaScript
9
star
51

fly-varnish

Dockerfile
9
star
52

grafana

Run Grafana on Fly
8
star
53

ollama-demo

@jmorganca's ollama.ai demo app on Fly.io
8
star
54

postgres-importer

Shell
8
star
55

nodejs-planetscale-read-replicas

A sample Node.js app that uses a Planetscale database with additional read-only regions
JavaScript
7
star
56

kong-api-gateway

Dockerfile
7
star
57

hostnamesapi

JavaScript examples for working with the new hostnames API on Fly
JavaScript
7
star
58

appkata-minio

MinIO S3-compatible storage on Fly
Dockerfile
7
star
59

autoscale-to-zero-demo

TypeScript
7
star
60

live-elements-demo

Live Elements Demo
Ruby
6
star
61

6pn-demo-chat

A Node-based websockets and NATS chat app which uses Fly 6PN networking
JavaScript
6
star
62

rqlite

Shell
6
star
63

deno-apollo

TypeScript
6
star
64

hello-fly

JavaScript
6
star
65

ssh-app

Run an SSH server to connect your privately networked Database apps to things like BI tools
Dockerfile
6
star
66

global-apollo-server

Fly global deployment with Apollo Server and Prisma
TypeScript
6
star
67

rails-nix

Deploy Rails apps on Fly.io with Nix
Ruby
5
star
68

code-server-dev-environment

Dockerfile
5
star
69

hello-flask

Example project demonstrating how to deploy a Flask app to Fly.io.
HTML
5
star
70

postgres

Deploy a Postgres database on Fly, ready for your Fly apps.
4
star
71

appkata-mqtt

An MQTT server app (with Mosquitto) with TLS+passwords
Dockerfile
4
star
72

global-rails

Ruby
4
star
73

globe-request-mapper

Elixir
4
star
74

buildkite-agent

Run a Buildkite agent on Fly with disk caching
Dockerfile
4
star
75

openresty-basic

Dockerfile
4
star
76

flydictionary

A light crud example for database examples
JavaScript
4
star
77

grpc-service

Running gRPC services on Fly.io
JavaScript
4
star
78

elixir_prom_ex_example

Elixir
4
star
79

hello-create-react-app

JavaScript
4
star
80

hello-remix

Sample Remix app setup for deployment on Fly.io
JavaScript
3
star
81

tcp-echo

TCP echo service for testing things that TCP
Go
3
star
82

rails-on-fly

Ruby
3
star
83

laravel-worker

Auto-scaled Laravel queue workers on Fly.io
PHP
3
star
84

pi-hole

Dockerfile
3
star
85

fly-nestjs

Example NestJS application configured for deployment on Fly.io
TypeScript
3
star
86

view-component-playground

Various view components tried in Rails
Ruby
3
star
87

python_gpu_example

A setup with Jupyter for GPU-enabled ML tinkering
Shell
3
star
88

hello-django

Example project demonstrating how to deploy a Django app to Fly.io.
Python
3
star
89

localai-demo

LocalAI demo app on Fly.io
Shell
3
star
90

rails-statics

Rails application to test the performance of Rails static assets
Ruby
3
star
91

hello-django-postgres

Python
2
star
92

fly-laravel-litefs

Guide on deploying multiple instances of a Laravel Fly app and integrating LiteFS and fly-replay to allow syncing SQLite database across the instances.
PHP
2
star
93

ollama-webui-demo

Shell
2
star
94

udp-echo-

Sample TCP/UDP Echo Service
Go
2
star
95

replicache-websocket

TypeScript
2
star
96

rails-machine-workers

A demonstration of how to use Fly Machines for "scale-to-0" ActiveJob background workers
Ruby
2
star
97

flygreeting

An example app for other examples to use.
Go
2
star
98

postgres-standalone

Standalone Postgres on Fly
Go
2
star
99

flychat-ws

A chat example using raw websockets, tested on Fly.io
JavaScript
2
star
100

fly-lucky

Crystal Lucky Framework app for Fly.io deployment
Crystal
2
star