Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
grepsedawk committed Feb 13, 2022
0 parents commit 2a40ab9
Show file tree
Hide file tree
Showing 13 changed files with 308 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
root = true

[*.cr]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
11 changes: 11 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates

version: 2

updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
open-pull-requests-limit: 5
target-branch: main
14 changes: 14 additions & 0 deletions .github/workflows/automerge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: Auto-merge minor/patch
on:
schedule:
- cron: "0 * * * *"
jobs:
test:
name: Auto-merge minor and patch updates
runs-on: ubuntu-latest
steps:
- uses: koj-co/dependabot-pr-action@master
with:
token: ${{ secrets.GITHUB_TOKEN }}
merge-minor: true
merge-patch: true
80 changes: 80 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: Continuous Integration

on:
push:
branches:
- main
pull_request:

jobs:
CheckFormat:
runs-on: ubuntu-latest
steps:
- uses: actions/[email protected]

- uses: oprypin/install-crystal@v1
with:
crystal: latest

- name: Format
run: crystal tool format --check

- name: Set up Crystal cache
uses: actions/[email protected]
id: crystal-cache
with:
path: |
~/.cache/crystal
bin/ameba
lib
key: ${{ runner.os }}-crystal-${{ hashFiles('**/shard.yml') }}
restore-keys: |
${{ runner.os }}-crystal-
- name: Install shards
if: steps.crystal-cache.outputs.cache-hit != 'true'
run: shards check || shards install --ignore-crystal-version

- name: Run ameba linter
run: ./bin/ameba

RunSpecs:
runs-on: ubuntu-latest

strategy:
fail-fast: true
matrix:
crystal_version:
- 1.2.0
- latest
experimental:
- false
include:
- crystal_version: nightly
experimental: true

steps:
- uses: actions/[email protected]

- uses: oprypin/install-crystal@v1
with:
crystal: ${{ matrix.crystal_version }}

- name: Set up Crystal cache
uses: actions/[email protected]
id: crystal-cache
with:
path: |
~/.cache/crystal
bin/ameba
lib
key: ${{ runner.os }}-crystal-${{ matrix.crystal_version }}-${{ hashFiles('**/shard.yml') }}
restore-keys: |
${{ runner.os }}-crystal-
- name: Install shards
if: steps.crystal-cache.outputs.cache-hit != 'true'
run: shards check || shards install --ignore-crystal-version

- name: Run tests
run: crystal spec
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/docs/
/lib/
/bin/
/.shards/
*.dwarf

# Libraries don't need dependency lock
# Dependencies will be locked in applications that use them
/shard.lock
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2022 Alex Piechowski <[email protected]>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
89 changes: 89 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# fly.cr

[Fly.io multi-region postgres support](https://fly.io/docs/getting-started/multi-region-databases)
for crystal.

## Installation

1. Add the dependency to your `shard.yml`:

```yaml
dependencies:
fly:
github: grepsedawk/fly.cr
version: ~> 0.1
```
2. Run `shards install`

## Usage

### Postgres via Avram

1. Add [Multi-region postgres](https://fly.io/docs/getting-started/multi-region-databases/#add-read-replicas)
regional database readers and multi-region web application deployments via fly.io
1. Configure primary (writer) region by setting ENV `$PRIMARY_REGION`
1. Require `fly/pg/error_handler` and `fly/avram` _after_ avram

```crystal
# ...
require "avram"
# ...
require "fly/pg/error_handler"
require "fly/avram"
# ...
```

This will add patching into avram in order to automatically change the port
to read only port (5433) when the `ENV["FLY_REGION"]`, set automatically
by fly.io, based on your `Fly` configuration.

1. Add middleware to your HTTP stack. This supports any core Crystal HTTP server.
The middleware must appear after any Error Handler middleware, such as the
`Lucky::ErrorHandler`.

For Lucky, this looks like this:

```crystal
def middleware : Array(HTTP::Handler)
[
# ...
Lucky::ErrorHandler.new(action: Errors::Show),
Raven::Lucky::ErrorHandler.new,
Fly::PG::ErrorHandler.new,
# ..,.
] of HTTP::Handler
end
```

## Configuration (Optional)

To override the use of `ENV['PRIMARY_REGION']`, you can set the Fly primary region
by writing a fly config:

```crystal
# config/fly.cr
Fly.configure do |settings|
settings.primary_region = ENV["WRITER_REGION"] # Can be any string
end
```

## Development

For now, make a Fly.io deploy using [lucky_jumpstart](https://github.com/stephendolan/lucky_jumpstart)
and use that to test.

It's a bit of a pain because deploys take 3-5min, but it's the best development
option as it provides integration testing.

## Contributing

1. Fork it (<https://github.com/grepsedawk/fly-crystal/fork>)
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create a new Pull Request

## Contributors

- [Alex Piechowski](https://github.com/grepsedawk) - creator and maintainer
17 changes: 17 additions & 0 deletions shard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: fly
version: 0.1.0

authors:
- Alex Piechowski <[email protected]>

crystal: ">=1"

license: MIT

dependencies:
habitat:
github: luckyframework/habitat

development_dependencies:
ameba:
github: crystal-ameba/ameba
7 changes: 7 additions & 0 deletions spec/fly_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
require "./spec_helper"

describe Fly do
it "has a version" do
Fly::VERSION.should_not be_nil
end
end
4 changes: 4 additions & 0 deletions spec/spec_helper.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
require "spec"
require "../src/fly"
require "../src/fly/pg/error_handler"
require "../src/fly/avram"
13 changes: 13 additions & 0 deletions src/fly.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# TODO: Write documentation for `Fly::Crystal`

require "habitat"
require "http/server"

module Fly
VERSION = "0.1.0"
Log = ::Log.for(self)

Habitat.create do
setting primary_region : String = ENV["PRIMARY_REGION"]
end
end
11 changes: 11 additions & 0 deletions src/fly/avram.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
require "../fly"

class Avram::Credentials
private def set_url_port(io)
if Fly.settings.primary_region == ENV["FLY_REGION"]
previous_def(io)
else
io << ":5433"
end
end
end
23 changes: 23 additions & 0 deletions src/fly/pg/error_handler.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
require "../../fly"

class Fly::PG::ErrorHandler
include HTTP::Handler

delegate primary_region, to: Fly.settings

def call(context : HTTP::Server::Context)
call_next(context)
rescue error : PQ::PQError
raise error unless read_only_sql_transaction?(error)

Log.info { "Replaying request in region #{primary_region}" }
context.response.status_code = 409
context.response.headers["fly-replay"] = "region=#{primary_region}"
end

def read_only_sql_transaction?(error : PQ::PQError)
error.fields.find do |field|
field.message == "25006" && field.name == :code
end
end
end

0 comments on commit 2a40ab9

Please sign in to comment.