litestream 0.14.0-x86_64-linux → 0.16.0-x86_64-linux
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +82 -66
- data/app/controllers/litestream/application_controller.rb +2 -0
- data/app/views/litestream/processes/show.html.erb +43 -55
- data/exe/x86_64-linux/litestream +0 -0
- data/exe/x86_64-linux/litestream.sha256 +1 -0
- data/lib/litestream/commands.rb +121 -26
- data/lib/litestream/generators/litestream/templates/config.yml.erb +11 -6
- data/lib/litestream/upstream.rb +15 -4
- data/lib/litestream/version.rb +1 -1
- data/lib/litestream.rb +71 -32
- data/lib/puma/plugin/litestream.rb +2 -14
- data/lib/tasks/litestream_tasks.rake +33 -16
- metadata +10 -17
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1eea2243f44a53621bf8fa287f9b27e8471da73646a4f5d2db4a63402f42c30d
|
|
4
|
+
data.tar.gz: c759e8b2f48804096e02191e2150de4f685f066d861c5f3aefb07310eb275805
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 94f95bf5a0239fd400b4ae031c99349db0c1297910fab2fa3a2883e05568ac9e0cfba5d5085812205d0bf4003100df0a478c36e011a67fec20571e11dde805e5
|
|
7
|
+
data.tar.gz: 82ad7f444fce743f8212f42a2c1d5196b44af2b845301441da014d44035f3ea8504a49c99cf8dab3edc55ba1779b11f7257ff584e184c2529c2829326e65e8f5
|
data/README.md
CHANGED
|
@@ -85,16 +85,22 @@ The gem streamlines the configuration process by providing a default configurati
|
|
|
85
85
|
The default configuration file looks like this if you only have one SQLite database:
|
|
86
86
|
|
|
87
87
|
```yaml
|
|
88
|
+
snapshot:
|
|
89
|
+
interval: 24h
|
|
90
|
+
retention: 24h
|
|
91
|
+
|
|
88
92
|
dbs:
|
|
89
93
|
- path: storage/production.sqlite3
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
replica:
|
|
95
|
+
type: s3
|
|
96
|
+
path: storage/production.sqlite3
|
|
97
|
+
bucket: $LITESTREAM_REPLICA_BUCKET
|
|
98
|
+
access-key-id: $LITESTREAM_ACCESS_KEY_ID
|
|
99
|
+
secret-access-key: $LITESTREAM_SECRET_ACCESS_KEY
|
|
96
100
|
```
|
|
97
101
|
|
|
102
|
+
In Litestream 0.5, snapshot interval and retention are global settings rather than per-replica settings.
|
|
103
|
+
|
|
98
104
|
This is the default for Amazon S3. The full range of possible replica types (e.g. other S3-compatible object storage servers) are covered in Litestream's [replica guides](https://litestream.io/guides/#replica-guides).
|
|
99
105
|
|
|
100
106
|
The gem also provides a default initializer file at `config/initializers/litestream.rb` that allows you to configure various variables referenced in the configuration file in Ruby. By providing a Ruby interface to these environment variables, you can use your preferred method of storing secrets. For example, the default generated file uses Rails' encrypted credentials to store your secrets.
|
|
@@ -126,6 +132,28 @@ end
|
|
|
126
132
|
|
|
127
133
|
However, if you need manual control over the Litestream configuration, you can edit the `config/litestream.yml` file. The full range of possible configurations are covered in Litestream's [configuration reference](https://litestream.io/reference/config/).
|
|
128
134
|
|
|
135
|
+
#### Logging
|
|
136
|
+
|
|
137
|
+
Litestream writes its logs to stdout, where they mix with your application's. Its `logging:` block can make them easy to separate, for example as JSON on stderr:
|
|
138
|
+
|
|
139
|
+
```yaml
|
|
140
|
+
logging:
|
|
141
|
+
level: info
|
|
142
|
+
type: json
|
|
143
|
+
stderr: true
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
#### Replicas that authenticate with a credentials file
|
|
147
|
+
|
|
148
|
+
Some replica types read credentials from a file. Google Cloud Storage, for example, reads the path in `GOOGLE_APPLICATION_CREDENTIALS`. A file on your machine is not in the container that Kamal deploys, so pass its contents as a secret and write it out before the app starts, for example in `bin/docker-entrypoint`:
|
|
149
|
+
|
|
150
|
+
```shell
|
|
151
|
+
# .kamal/secrets: GCS_CREDENTIALS_JSON=$(cat config/gcs.json)
|
|
152
|
+
# config/deploy.yml: env: { secret: [GCS_CREDENTIALS_JSON] }
|
|
153
|
+
printf '%s' "$GCS_CREDENTIALS_JSON" > /rails/tmp/gcs.json
|
|
154
|
+
export GOOGLE_APPLICATION_CREDENTIALS=/rails/tmp/gcs.json
|
|
155
|
+
```
|
|
156
|
+
|
|
129
157
|
### Replication
|
|
130
158
|
|
|
131
159
|
In order to stream changes to your configured replicas, you need to start the Litestream replication process.
|
|
@@ -179,7 +207,7 @@ You can restore any replicated database at any point using the gem's provided `l
|
|
|
179
207
|
> [!NOTE]
|
|
180
208
|
> During the restoration process, you need to prevent any interaction with ActiveRecord/SQLite, such as from a running `rails server` or `rails console` instance. If there is any interaction, Rails might regenerate the production database and prevent restoration via litestream. If this happens, you might get a "cannot restore, output path already exists" error.
|
|
181
209
|
|
|
182
|
-
1. Rename the production
|
|
210
|
+
1. Rename the production database and its SQLite sidecar files (**recommended**) or alternatively delete them. To delete the production databases locally, you can run the following at your own risk:
|
|
183
211
|
```shell
|
|
184
212
|
# DANGEROUS OPERATION, consider renaming database files instead
|
|
185
213
|
bin/rails db:drop DISABLE_DATABASE_ENVIRONMENT_CHECK=1
|
|
@@ -215,26 +243,19 @@ You can forward arguments in whatever order you like, you simply need to ensure
|
|
|
215
243
|
-if-replica-exists
|
|
216
244
|
Returns exit code of 0 if no backups found.
|
|
217
245
|
|
|
218
|
-
-
|
|
219
|
-
|
|
220
|
-
Defaults to 8
|
|
221
|
-
|
|
222
|
-
-replica NAME
|
|
223
|
-
Restore from a specific replica.
|
|
224
|
-
Defaults to replica with latest data.
|
|
225
|
-
|
|
226
|
-
-generation NAME
|
|
227
|
-
Restore from a specific generation.
|
|
228
|
-
Defaults to generation with latest data.
|
|
229
|
-
|
|
230
|
-
-index NUM
|
|
231
|
-
Restore up to a specific WAL index (inclusive).
|
|
232
|
-
Defaults to use the highest available index.
|
|
246
|
+
-txid TXID
|
|
247
|
+
Restore through a specific 16-character hexadecimal transaction ID.
|
|
233
248
|
|
|
234
249
|
-timestamp TIMESTAMP
|
|
235
250
|
Restore to a specific point-in-time.
|
|
236
251
|
Defaults to use the latest available backup.
|
|
237
252
|
|
|
253
|
+
-json
|
|
254
|
+
Print the restore result as JSON.
|
|
255
|
+
|
|
256
|
+
-dry-run
|
|
257
|
+
Print the restore plan without writing the database.
|
|
258
|
+
|
|
238
259
|
-config PATH
|
|
239
260
|
Specifies the configuration file.
|
|
240
261
|
Defaults to /etc/litestream.yml
|
|
@@ -387,79 +408,59 @@ bin/rails litestream:databases
|
|
|
387
408
|
This will return a list of databases and their configured replicas:
|
|
388
409
|
|
|
389
410
|
```
|
|
390
|
-
path
|
|
411
|
+
path replica
|
|
391
412
|
/Users/you/Code/your-app/storage/production.sqlite3 s3
|
|
392
413
|
```
|
|
393
414
|
|
|
394
|
-
You can
|
|
415
|
+
You can inspect local replication status for all databases or one specific database:
|
|
395
416
|
|
|
396
417
|
```shell
|
|
397
|
-
bin/rails litestream:
|
|
418
|
+
bin/rails litestream:status -- --database=storage/production.sqlite3
|
|
398
419
|
```
|
|
399
420
|
|
|
400
|
-
This
|
|
421
|
+
This reads local state and does not require a running Litestream process:
|
|
401
422
|
|
|
402
423
|
```
|
|
403
|
-
|
|
404
|
-
|
|
424
|
+
database status local txid wal size
|
|
425
|
+
/Users/you/Code/your-app/storage/production.sqlite3 ok 000000000000000a 128 kB
|
|
405
426
|
```
|
|
406
427
|
|
|
407
|
-
You can list
|
|
428
|
+
You can list all remote LTX files, including the level-9 snapshot:
|
|
408
429
|
|
|
409
430
|
```shell
|
|
410
|
-
bin/rails litestream:
|
|
431
|
+
bin/rails litestream:ltx -- --database=storage/production.sqlite3 --level=all
|
|
411
432
|
```
|
|
412
433
|
|
|
413
|
-
|
|
434
|
+
The command returns the compaction level, transaction range, byte size, and creation time:
|
|
414
435
|
|
|
415
436
|
```
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
```
|
|
419
|
-
|
|
420
|
-
Finally, you can list the wal files available for a database:
|
|
421
|
-
|
|
422
|
-
```shell
|
|
423
|
-
bin/rails litestream:wal -- --database=storage/production.sqlite3
|
|
424
|
-
```
|
|
425
|
-
|
|
426
|
-
This command lists wal files available for that specified database:
|
|
427
|
-
|
|
428
|
-
```
|
|
429
|
-
replica generation index offset size created
|
|
430
|
-
s3 a295b16a796689f3 1 0 2036 2024-04-17T00:01:19Z
|
|
437
|
+
level min_txid max_txid size created
|
|
438
|
+
0 0000000000000008 000000000000000a 1013 2026-09-08T03:16:43Z
|
|
431
439
|
```
|
|
432
440
|
|
|
433
441
|
### Running commands from Ruby
|
|
434
442
|
|
|
435
443
|
In addition to the provided rake tasks, you can also run Litestream commands directly from Ruby. The gem provides a `Litestream::Commands` module that wraps the Litestream CLI commands. This is particularly useful for the introspection commands, as you can use the output in your Ruby code.
|
|
436
444
|
|
|
437
|
-
The `Litestream::Commands.databases` method returns
|
|
445
|
+
Pass `json: true` to return the 0.5 CLI's JSON values with string keys. The `Litestream::Commands.databases` method returns each database path and replica type:
|
|
438
446
|
|
|
439
447
|
```ruby
|
|
440
|
-
Litestream::Commands.databases
|
|
441
|
-
# => [{"path"=>"/Users/you/Code/your-app/storage/production.sqlite3", "
|
|
448
|
+
Litestream::Commands.databases(json: true)
|
|
449
|
+
# => [{"path"=>"/Users/you/Code/your-app/storage/production.sqlite3", "replica"=>"s3"}]
|
|
442
450
|
```
|
|
443
451
|
|
|
444
|
-
The `
|
|
452
|
+
The `status` method returns local state without connecting to the replica or requiring a daemon:
|
|
445
453
|
|
|
446
454
|
```ruby
|
|
447
|
-
Litestream::Commands.
|
|
448
|
-
# => [{"
|
|
455
|
+
Litestream::Commands.status("storage/production.sqlite3", json: true)
|
|
456
|
+
# => [{"database"=>"/Users/you/Code/your-app/storage/production.sqlite3", "status"=>"ok", "local_txid"=>"000000000000000a", "wal_size"=>"128 kB"}]
|
|
449
457
|
```
|
|
450
458
|
|
|
451
|
-
The `
|
|
459
|
+
The `ltx` method lists remote LTX files. An unreplicated database returns an empty array:
|
|
452
460
|
|
|
453
461
|
```ruby
|
|
454
|
-
Litestream::Commands.
|
|
455
|
-
# => [{"
|
|
456
|
-
```
|
|
457
|
-
|
|
458
|
-
The `Litestream::Commands.wal` method returns an array of hashes with the "replica", "generation", "index", "offset","size", and "created" keys for each wal:
|
|
459
|
-
|
|
460
|
-
```ruby
|
|
461
|
-
Litestream::Commands.wal('storage/production.sqlite3')
|
|
462
|
-
# => [{"replica"=>"s3", "generation"=>"5f4341bc3d22d615", "index"=>"0", "offset"=>"0", "size"=>"2036", "created"=>"2024-04-17T19:48:09Z"}]
|
|
462
|
+
Litestream::Commands.ltx("storage/production.sqlite3", json: true, "--level" => "all")
|
|
463
|
+
# => [{"level"=>0, "min_txid"=>"0000000000000008", "max_txid"=>"000000000000000a", "size"=>1013, "timestamp"=>"2026-09-08T03:16:43Z"}]
|
|
463
464
|
```
|
|
464
465
|
|
|
465
466
|
You can also restore a database programmatically using the `Litestream::Commands.restore` method, which returns the path to the restored database:
|
|
@@ -479,14 +480,29 @@ The full set of commands available to the `litestream` executable are covered in
|
|
|
479
480
|
|
|
480
481
|
```shell
|
|
481
482
|
litestream databases [arguments]
|
|
482
|
-
litestream
|
|
483
|
+
litestream info [arguments]
|
|
484
|
+
litestream list [arguments]
|
|
485
|
+
litestream ltx [arguments] DB_PATH
|
|
486
|
+
litestream register [arguments]
|
|
483
487
|
litestream replicate [arguments]
|
|
484
|
-
litestream
|
|
485
|
-
litestream
|
|
488
|
+
litestream reset [arguments]
|
|
489
|
+
litestream restore [arguments] DB_PATH
|
|
490
|
+
litestream start [arguments]
|
|
491
|
+
litestream status [arguments] [DB_PATH]
|
|
492
|
+
litestream stop [arguments]
|
|
493
|
+
litestream sync [arguments]
|
|
494
|
+
litestream unregister [arguments]
|
|
486
495
|
litestream version
|
|
487
|
-
litestream wal [arguments] DB_PATH|REPLICA_URL
|
|
488
496
|
```
|
|
489
497
|
|
|
498
|
+
### Upgrading from 0.3
|
|
499
|
+
|
|
500
|
+
Litestream 0.5 replicates each database to a single `replica:` map and moves retention and snapshot settings into the root-level `snapshot:` block. A `replicas:` list with one entry still works. A list with more than one entry stops `litestream replicate` from starting ("multiple replicas on a single database are no longer supported"), and when the Puma plugin sees Litestream exit it stops Puma, so reduce each database to one replica before upgrading. The `databases` command and Ruby method now return a `"replica"` key instead of `"replicas"`.
|
|
501
|
+
|
|
502
|
+
The removed `generations`, `snapshots`, and `wal` command methods and rake tasks now raise an error pointing to `ltx`, which replaces all three forms of remote backup introspection.
|
|
503
|
+
|
|
504
|
+
The 0.5 `restore` command auto-detects both 0.3 and LTX backups and prefers whichever backup is newer. Existing 0.3 `generations/` objects are left in place; you can delete them after the LTX history covers your full retention window. To roll back, reinstall a 0.15.x release of this gem, which bundles Litestream 0.3.
|
|
505
|
+
|
|
490
506
|
### Using in development
|
|
491
507
|
|
|
492
508
|
By default, if you install the gem and configure via `puma.rb` or `Procfile`, Litestream will not start in development.
|
|
@@ -56,63 +56,51 @@
|
|
|
56
56
|
</div>
|
|
57
57
|
|
|
58
58
|
<br />
|
|
59
|
-
<section
|
|
60
|
-
<% database['
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
<section class="ml-6">
|
|
60
|
+
<% if database['error'] %>
|
|
61
|
+
<p class="text-red-600 dark:text-red-400"><%= database['error'] %></p>
|
|
62
|
+
<% else %>
|
|
63
|
+
<dl class="grid grid-cols-[fit-content(100%)_1fr] gap-x-4">
|
|
64
|
+
<dt class="font-bold">Status</dt>
|
|
65
|
+
<dd><%= database.dig('status', 'status') %></dd>
|
|
66
|
+
<dt class="font-bold">Local TXID</dt>
|
|
67
|
+
<dd><code><%= database.dig('status', 'local_txid') %></code></dd>
|
|
68
|
+
<dt class="font-bold">WAL size</dt>
|
|
69
|
+
<dd><%= number_to_human_size database.dig('status', 'wal_size') %></dd>
|
|
70
|
+
</dl>
|
|
66
71
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
<br />
|
|
73
|
+
<% if database['ltx'] == [] %>
|
|
74
|
+
<p>No LTX files yet</p>
|
|
75
|
+
<% else %>
|
|
76
|
+
<table class="min-w-full divide-y divide-gray-300">
|
|
77
|
+
<thead>
|
|
78
|
+
<tr>
|
|
79
|
+
<th scope="col" class="whitespace-nowrap px-2 py-2 text-right text-sm font-semibold text-gray-900 dark:text-gray-100">Level</th>
|
|
80
|
+
<th scope="col" class="whitespace-nowrap px-2 py-2 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">Min TXID</th>
|
|
81
|
+
<th scope="col" class="whitespace-nowrap px-2 py-2 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">Max TXID</th>
|
|
82
|
+
<th scope="col" class="whitespace-nowrap px-2 py-2 text-right text-sm font-semibold text-gray-900 dark:text-gray-100">Size</th>
|
|
83
|
+
<th scope="col" class="whitespace-nowrap px-2 py-2 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">Created</th>
|
|
84
|
+
</tr>
|
|
85
|
+
</thead>
|
|
74
86
|
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
</thead>
|
|
93
|
-
|
|
94
|
-
<tbody class="bg-white dark:bg-gray-900">
|
|
95
|
-
<% generation['snapshots'].each do |snapshot| %>
|
|
96
|
-
<tr class="align-top even:bg-gray-50 dark:even:bg-gray-800">
|
|
97
|
-
<td scope="col" class="whitespace-nowrap px-2 py-2 text-sm text-gray-900 dark:text-gray-100">
|
|
98
|
-
<abbr title="<%= snapshot['created'] %>" class="underline decoration-dashed decoration-gray-500 cursor-help">
|
|
99
|
-
<time datetime="<%= snapshot['created'] %>"><%= DateTime.parse(snapshot['created']).to_formatted_s(:db) %></time>
|
|
100
|
-
</abbr>
|
|
101
|
-
</td>
|
|
102
|
-
<td scope="col" class="whitespace-nowrap px-2 py-2 text-sm text-gray-900 dark:text-gray-100 text-right">
|
|
103
|
-
<%= snapshot['index'] %>
|
|
104
|
-
</td>
|
|
105
|
-
<td scope="col" class="whitespace-nowrap px-2 py-2 text-sm text-gray-900 dark:text-gray-100 text-right">
|
|
106
|
-
<%= number_to_human_size snapshot['size'] %>
|
|
107
|
-
</td>
|
|
108
|
-
</tr>
|
|
109
|
-
<% end %>
|
|
110
|
-
</tbody>
|
|
111
|
-
</table>
|
|
112
|
-
</dd>
|
|
113
|
-
</div>
|
|
114
|
-
</dl>
|
|
115
|
-
</details>
|
|
87
|
+
<tbody class="bg-white dark:bg-gray-900">
|
|
88
|
+
<% database['ltx'].each do |ltx| %>
|
|
89
|
+
<tr class="align-top even:bg-gray-50 dark:even:bg-gray-800">
|
|
90
|
+
<td class="whitespace-nowrap px-2 py-2 text-sm text-gray-900 dark:text-gray-100 text-right"><%= ltx['level'] %></td>
|
|
91
|
+
<td class="whitespace-nowrap px-2 py-2 text-sm text-gray-900 dark:text-gray-100"><code><%= ltx['min_txid'] %></code></td>
|
|
92
|
+
<td class="whitespace-nowrap px-2 py-2 text-sm text-gray-900 dark:text-gray-100"><code><%= ltx['max_txid'] %></code></td>
|
|
93
|
+
<td class="whitespace-nowrap px-2 py-2 text-sm text-gray-900 dark:text-gray-100 text-right"><%= number_to_human_size ltx['size'] %></td>
|
|
94
|
+
<td class="whitespace-nowrap px-2 py-2 text-sm text-gray-900 dark:text-gray-100">
|
|
95
|
+
<abbr title="<%= ltx['timestamp'] %>" class="underline decoration-dashed decoration-gray-500 cursor-help">
|
|
96
|
+
<time datetime="<%= ltx['timestamp'] %>"><%= DateTime.parse(ltx['timestamp']).to_formatted_s(:db) %></time>
|
|
97
|
+
</abbr>
|
|
98
|
+
</td>
|
|
99
|
+
</tr>
|
|
100
|
+
<% end %>
|
|
101
|
+
</tbody>
|
|
102
|
+
</table>
|
|
103
|
+
<% end %>
|
|
116
104
|
<% end %>
|
|
117
105
|
</section>
|
|
118
106
|
</li>
|
data/exe/x86_64-linux/litestream
CHANGED
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cfb371176d164437ae869f8351cfde49bd1804ae71c61923f75c9cba9c9c006d
|
data/lib/litestream/commands.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "open3"
|
|
1
3
|
require_relative "upstream"
|
|
2
4
|
|
|
3
5
|
module Litestream
|
|
@@ -20,6 +22,9 @@ module Litestream
|
|
|
20
22
|
# raised when a litestream command fails
|
|
21
23
|
CommandFailedException = Class.new(StandardError)
|
|
22
24
|
|
|
25
|
+
# raised when a litestream command times out
|
|
26
|
+
CommandTimeoutException = Class.new(CommandFailedException)
|
|
27
|
+
|
|
23
28
|
module Output
|
|
24
29
|
class << self
|
|
25
30
|
def format(data)
|
|
@@ -47,7 +52,10 @@ module Litestream
|
|
|
47
52
|
litestream_install_dir = ENV["LITESTREAM_INSTALL_DIR"]
|
|
48
53
|
if litestream_install_dir
|
|
49
54
|
if File.directory?(litestream_install_dir)
|
|
50
|
-
|
|
55
|
+
unless @litestream_install_dir_noted
|
|
56
|
+
warn "NOTE: using LITESTREAM_INSTALL_DIR to find litestream executable: #{litestream_install_dir}"
|
|
57
|
+
@litestream_install_dir_noted = true
|
|
58
|
+
end
|
|
51
59
|
exe_path = litestream_install_dir
|
|
52
60
|
exe_file = File.expand_path(File.join(litestream_install_dir, "litestream"))
|
|
53
61
|
else
|
|
@@ -94,12 +102,14 @@ module Litestream
|
|
|
94
102
|
exe_file
|
|
95
103
|
end
|
|
96
104
|
|
|
97
|
-
# Replicate
|
|
98
|
-
# Puma
|
|
99
|
-
#
|
|
105
|
+
# Replicate runs either as a child process or in the foreground, depending on the context.
|
|
106
|
+
# The Puma plugin starts it as a child and gets its pid back; the rake task runs it in the
|
|
107
|
+
# foreground and streams its output.
|
|
100
108
|
def replicate(async: false, **argv)
|
|
101
109
|
cmd = prepare("replicate", argv)
|
|
102
110
|
run_replicate(cmd, async: async)
|
|
111
|
+
rescue CommandFailedException
|
|
112
|
+
raise
|
|
103
113
|
rescue
|
|
104
114
|
raise CommandFailedException, "Failed to execute `#{cmd.join(" ")}`"
|
|
105
115
|
end
|
|
@@ -114,35 +124,48 @@ module Litestream
|
|
|
114
124
|
execute("databases", argv)
|
|
115
125
|
end
|
|
116
126
|
|
|
117
|
-
def
|
|
118
|
-
raise DatabaseRequiredException, "database argument is required for
|
|
127
|
+
def ltx(database, **argv)
|
|
128
|
+
raise DatabaseRequiredException, "database argument is required for ltx command, e.g. litestream:ltx -- --database=path/to/database.sqlite" if database.nil?
|
|
119
129
|
|
|
120
|
-
execute("
|
|
130
|
+
execute("ltx", argv, database)
|
|
121
131
|
end
|
|
122
132
|
|
|
123
|
-
|
|
124
|
-
|
|
133
|
+
# Litestream 0.5 filters `status` by the absolute database path, while
|
|
134
|
+
# `ltx` and `restore` match the path as written in the config file, so a
|
|
135
|
+
# relative path is expanded here to keep the documented
|
|
136
|
+
# `--database=storage/production.sqlite3` form working.
|
|
137
|
+
def status(database = nil, **argv)
|
|
138
|
+
execute("status", argv, database && File.expand_path(database))
|
|
139
|
+
end
|
|
125
140
|
|
|
126
|
-
|
|
141
|
+
def generations(*)
|
|
142
|
+
raise CommandFailedException, "`generations` was removed in Litestream 0.5; use `ltx` (see README, Upgrading from 0.3)"
|
|
127
143
|
end
|
|
128
144
|
|
|
129
|
-
def
|
|
130
|
-
raise
|
|
145
|
+
def snapshots(*)
|
|
146
|
+
raise CommandFailedException, "`snapshots` was removed in Litestream 0.5; use `ltx` (see README, Upgrading from 0.3)"
|
|
147
|
+
end
|
|
131
148
|
|
|
132
|
-
|
|
149
|
+
def wal(*)
|
|
150
|
+
raise CommandFailedException, "`wal` was removed in Litestream 0.5; use `ltx` (see README, Upgrading from 0.3)"
|
|
133
151
|
end
|
|
134
152
|
|
|
135
153
|
private
|
|
136
154
|
|
|
137
155
|
def execute(command, argv = {}, database = nil, tabled_output: true)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
156
|
+
argv = argv.stringify_keys
|
|
157
|
+
timeout = argv.delete("timeout")
|
|
158
|
+
output = if argv.delete("json")
|
|
159
|
+
argv["-json"] = nil
|
|
160
|
+
:json
|
|
161
|
+
elsif tabled_output
|
|
162
|
+
:table
|
|
143
163
|
else
|
|
144
|
-
|
|
164
|
+
:raw
|
|
145
165
|
end
|
|
166
|
+
|
|
167
|
+
cmd = prepare(command, argv, database)
|
|
168
|
+
run(cmd, output: output, timeout: timeout)
|
|
146
169
|
end
|
|
147
170
|
|
|
148
171
|
def prepare(command, argv = {}, database = nil)
|
|
@@ -154,29 +177,101 @@ module Litestream
|
|
|
154
177
|
|
|
155
178
|
args = {
|
|
156
179
|
"--config" => Litestream.config_path.to_s
|
|
157
|
-
}.merge(argv.stringify_keys).to_a.flatten.compact
|
|
158
|
-
cmd = [executable, command, *args, database].compact
|
|
180
|
+
}.merge(argv.stringify_keys).to_a.flatten.compact.map(&:to_s)
|
|
181
|
+
cmd = [executable, command, *args, database].compact.map(&:to_s)
|
|
159
182
|
puts cmd.inspect if ENV["DEBUG"]
|
|
160
183
|
|
|
161
184
|
cmd
|
|
162
185
|
end
|
|
163
186
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
187
|
+
# Runs the command without a shell and returns its parsed stdout. A non-zero
|
|
188
|
+
# exit raises with stderr. With a timeout, the command runs in its own
|
|
189
|
+
# process group and is killed (TERM, then KILL) when the deadline passes.
|
|
190
|
+
def run(cmd, output:, timeout: nil)
|
|
191
|
+
stdin, stdout, stderr, wait_thread = Open3.popen3(*cmd, pgroup: true)
|
|
192
|
+
stdin.close
|
|
193
|
+
stdout_reader = Thread.new { stdout.read }
|
|
194
|
+
stderr_reader = Thread.new { stderr.read }
|
|
195
|
+
|
|
196
|
+
# The readers finish when the last process holding the pipes exits, so
|
|
197
|
+
# waiting on them covers descendants the direct child may have left behind.
|
|
198
|
+
unless wait_thread.join(timeout) && stdout_reader.join(timeout) && stderr_reader.join(timeout)
|
|
199
|
+
kill_process_group("TERM", wait_thread.pid)
|
|
200
|
+
wait_thread.join(1)
|
|
201
|
+
kill_process_group("KILL", wait_thread.pid)
|
|
202
|
+
wait_thread.join
|
|
203
|
+
[stdout_reader, stderr_reader].each(&:join)
|
|
204
|
+
raise CommandTimeoutException, "Failed to execute `#{cmd[1]}`: timed out after #{timeout} seconds"
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
status = wait_thread.value
|
|
208
|
+
unless status.success?
|
|
209
|
+
raise CommandFailedException, "Failed to execute `#{cmd[1]}` (exit status #{status.exitstatus}): #{stderr_reader.value.strip[0, 500]}"
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
case output
|
|
213
|
+
when :json then parse_json(cmd, stdout_reader.value)
|
|
214
|
+
when :table then parse_table(stdout_reader.value)
|
|
215
|
+
else stdout_reader.value
|
|
216
|
+
end
|
|
217
|
+
ensure
|
|
218
|
+
[stdout_reader, stderr_reader].each { |reader| reader&.join }
|
|
219
|
+
[stdin, stdout, stderr].each { |io| io&.close unless io&.closed? }
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# macOS answers EPERM rather than ESRCH when every process left in the
|
|
223
|
+
# group has exited but not yet been reaped.
|
|
224
|
+
def kill_process_group(signal, pid)
|
|
225
|
+
Process.kill(signal, -pid)
|
|
226
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Two opt-in restore skips (-if-db-not-exists when the output exists,
|
|
230
|
+
# -if-replica-exists with no backups) exit 0 and print one logfmt line on
|
|
231
|
+
# stdout instead of JSON. They come back as {"skipped" => true, "message" => ...}.
|
|
232
|
+
def parse_json(cmd, stdout)
|
|
233
|
+
stdout = stdout.strip
|
|
234
|
+
return if stdout.empty?
|
|
235
|
+
return JSON.parse(stdout) if stdout.start_with?("{", "[")
|
|
236
|
+
|
|
237
|
+
skipped = stdout.match(/\Atime=\S+ level=\S+ msg=(?:"([^"]*)"|(\S+))/)
|
|
238
|
+
return {"skipped" => true, "message" => skipped[1] || skipped[2]} if skipped
|
|
239
|
+
|
|
240
|
+
raise CommandFailedException, "Unexpected output from `#{cmd[1]}`: #{stdout.lines.first.to_s.strip[0, 200]}"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Litestream pads columns with at least two spaces, and some headers and
|
|
244
|
+
# values hold a single one ("local txid", "231 kB", "no database").
|
|
245
|
+
def parse_table(stdout)
|
|
246
|
+
keys, *rows = stdout.strip.split("\n").map { _1.strip.split(/\s{2,}/) }
|
|
247
|
+
return [] unless keys
|
|
167
248
|
|
|
168
|
-
keys, *rows = stdout.split("\n").map { _1.split(/\s+/) }
|
|
169
249
|
rows.map { keys.zip(_1).to_h }
|
|
170
250
|
end
|
|
171
251
|
|
|
252
|
+
# Async replication returns the pid of the litestream process itself so a
|
|
253
|
+
# supervisor can signal and wait on it. Process.spawn is fork-and-exec in
|
|
254
|
+
# one call, so the pid it returns is the one that becomes litestream. The
|
|
255
|
+
# previous `exec(*cmd) if fork.nil?` also execs in a child but returns
|
|
256
|
+
# into the parent too, and the Puma plugin wrapped that in a fork of its
|
|
257
|
+
# own, so it recorded a pid that exited immediately while the daemon was
|
|
258
|
+
# reparented to init and outlived Puma.
|
|
172
259
|
def run_replicate(cmd, async:)
|
|
173
260
|
if async
|
|
174
|
-
|
|
261
|
+
Process.spawn(*cmd)
|
|
175
262
|
else
|
|
176
263
|
# When running in-process, we capture output continuously and write to stdout.
|
|
177
264
|
IO.popen(cmd, err: [:child, :out]) do |io|
|
|
178
265
|
io.each_line { |line| puts line }
|
|
179
266
|
end
|
|
267
|
+
status = $?
|
|
268
|
+
# `replicate` runs until it is signalled, so a signal is how it is
|
|
269
|
+
# meant to end. A non-zero exit is litestream failing to start or
|
|
270
|
+
# dying on its own, which is invisible today: the task exits 0 and a
|
|
271
|
+
# supervisor sees a clean stop rather than a crash to restart.
|
|
272
|
+
if status && !status.success? && !status.signaled?
|
|
273
|
+
raise CommandFailedException, "Failed to execute `replicate` (exit status #{status.exitstatus})"
|
|
274
|
+
end
|
|
180
275
|
end
|
|
181
276
|
end
|
|
182
277
|
end
|
|
@@ -9,13 +9,18 @@
|
|
|
9
9
|
# `replicate` command.
|
|
10
10
|
#
|
|
11
11
|
# For more details, see: https://litestream.io/reference/config/
|
|
12
|
+
# Retention and snapshot settings are global in Litestream 0.5.
|
|
13
|
+
snapshot:
|
|
14
|
+
interval: 24h
|
|
15
|
+
retention: 24h
|
|
16
|
+
|
|
12
17
|
dbs:
|
|
13
18
|
<%- production_sqlite_databases.each do |database| -%>
|
|
14
19
|
- path: <%= database %>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
replica:
|
|
21
|
+
type: s3
|
|
22
|
+
bucket: $LITESTREAM_REPLICA_BUCKET
|
|
23
|
+
path: <%= database %>
|
|
24
|
+
access-key-id: $LITESTREAM_ACCESS_KEY_ID
|
|
25
|
+
secret-access-key: $LITESTREAM_SECRET_ACCESS_KEY
|
|
21
26
|
<%- end -%>
|
data/lib/litestream/upstream.rb
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
module Litestream
|
|
2
2
|
module Upstream
|
|
3
|
-
VERSION = "
|
|
3
|
+
VERSION = "0.5.17"
|
|
4
4
|
|
|
5
5
|
# rubygems platform name => upstream release filename
|
|
6
6
|
NATIVE_PLATFORMS = {
|
|
7
7
|
"aarch64-linux" => "litestream-#{VERSION}-linux-arm64.tar.gz",
|
|
8
|
-
"arm64-darwin" => "litestream-#{VERSION}-darwin-arm64.
|
|
8
|
+
"arm64-darwin" => "litestream-#{VERSION}-darwin-arm64.tar.gz",
|
|
9
9
|
"arm64-linux" => "litestream-#{VERSION}-linux-arm64.tar.gz",
|
|
10
|
-
"x86_64-darwin" => "litestream-#{VERSION}-darwin-
|
|
11
|
-
"x86_64-linux" => "litestream-#{VERSION}-linux-
|
|
10
|
+
"x86_64-darwin" => "litestream-#{VERSION}-darwin-x86_64.tar.gz",
|
|
11
|
+
"x86_64-linux" => "litestream-#{VERSION}-linux-x86_64.tar.gz"
|
|
12
12
|
}
|
|
13
|
+
|
|
14
|
+
CHECKSUMS = {
|
|
15
|
+
"litestream-0.5.17-linux-x86_64.tar.gz" => "cfb371176d164437ae869f8351cfde49bd1804ae71c61923f75c9cba9c9c006d",
|
|
16
|
+
"litestream-0.5.17-linux-arm64.tar.gz" => "f8ca4a050095c1efbda2c4365172e61bf9d955ea0d9ac42f448b52e51819baa5",
|
|
17
|
+
"litestream-0.5.17-darwin-x86_64.tar.gz" => "891875af09db152e93a4b31a8a79f538ce7ce702c132803cfe0a831e7cb1b7db",
|
|
18
|
+
"litestream-0.5.17-darwin-arm64.tar.gz" => "e211f68ff7658d19f193f2914417afdf8f89a053ff8f263e5d6b3b1d3bbc7b08"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
def self.download_url(filename)
|
|
22
|
+
"https://github.com/benbjohnson/litestream/releases/download/v#{VERSION}/#{filename}"
|
|
23
|
+
end
|
|
13
24
|
end
|
|
14
25
|
end
|
data/lib/litestream/version.rb
CHANGED
data/lib/litestream.rb
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "etc"
|
|
3
4
|
require "sqlite3"
|
|
5
|
+
require "time"
|
|
4
6
|
|
|
5
7
|
module Litestream
|
|
6
8
|
VerificationFailure = Class.new(StandardError)
|
|
7
9
|
|
|
10
|
+
PROCESS_STATES = {
|
|
11
|
+
"D" => "uninterruptible",
|
|
12
|
+
"I" => "idle",
|
|
13
|
+
"R" => "running",
|
|
14
|
+
"S" => "sleeping",
|
|
15
|
+
"T" => "stopped",
|
|
16
|
+
"U" => "uninterruptible",
|
|
17
|
+
"Z" => "zombie"
|
|
18
|
+
}.freeze
|
|
19
|
+
private_constant :PROCESS_STATES
|
|
20
|
+
|
|
8
21
|
class << self
|
|
9
22
|
attr_writer :configuration
|
|
10
23
|
|
|
@@ -107,20 +120,18 @@ module Litestream
|
|
|
107
120
|
end
|
|
108
121
|
|
|
109
122
|
def databases
|
|
110
|
-
databases = Commands.databases
|
|
123
|
+
databases = Commands.databases(json: true)
|
|
111
124
|
|
|
112
125
|
databases.each do |db|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
replica = generation["name"]
|
|
120
|
-
generation["snapshots"] = snapshots.select { |snapshot| snapshot["generation"] == id && snapshot["replica"] == replica }
|
|
121
|
-
.map { |s| s.slice("index", "size", "created") }
|
|
122
|
-
generation.slice("generation", "name", "lag", "start", "end", "snapshots")
|
|
126
|
+
path = db["path"]
|
|
127
|
+
begin
|
|
128
|
+
db["status"] = Commands.status(path, json: true).first
|
|
129
|
+
db["ltx"] = Commands.ltx(path, :json => true, "--level" => "all")
|
|
130
|
+
rescue Commands::CommandFailedException => error
|
|
131
|
+
db["error"] = error.message
|
|
123
132
|
end
|
|
133
|
+
|
|
134
|
+
db["path"] = path.gsub(Rails.root.to_s, "[ROOT]")
|
|
124
135
|
end
|
|
125
136
|
end
|
|
126
137
|
|
|
@@ -163,30 +174,58 @@ module Litestream
|
|
|
163
174
|
info
|
|
164
175
|
end
|
|
165
176
|
|
|
177
|
+
# Linux has every process under /proc, even in images that ship no `ps`
|
|
178
|
+
# (Debian slim) or BusyBox's, which lacks `-x` and `lstart` (Alpine).
|
|
166
179
|
def process_info
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
return unless exit_code.zero?
|
|
180
|
+
File.directory?("/proc/self") ? proc_process_info : ps_process_info
|
|
181
|
+
end
|
|
170
182
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
183
|
+
def proc_process_info(proc_dir = "/proc")
|
|
184
|
+
boot_time = File.foreach(File.join(proc_dir, "stat")).grep(/\Abtime /).first.split.last.to_i
|
|
185
|
+
|
|
186
|
+
Dir.glob(File.join(proc_dir, "[0-9]*")).each do |dir|
|
|
187
|
+
executable, command = File.read(File.join(dir, "cmdline")).split("\0")
|
|
188
|
+
next unless File.basename(executable.to_s) == "litestream" && command == "replicate"
|
|
189
|
+
|
|
190
|
+
# Fields are counted after the command name's closing paren, since the
|
|
191
|
+
# name itself may contain spaces and parens.
|
|
192
|
+
state, *fields = File.read(File.join(dir, "stat")).rpartition(")").last.split
|
|
193
|
+
start_ticks = fields[18].to_i
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
pid: File.basename(dir),
|
|
197
|
+
status: PROCESS_STATES[state[0]],
|
|
198
|
+
started: Time.at(boot_time + start_ticks.fdiv(Etc.sysconf(Etc::SC_CLK_TCK))).to_datetime
|
|
199
|
+
}
|
|
200
|
+
rescue SystemCallError
|
|
201
|
+
# the process exited while being read, or belongs to another user
|
|
202
|
+
next
|
|
188
203
|
end
|
|
189
|
-
|
|
204
|
+
nil
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# macOS. The command name is matched on its own so that a shell or editor
|
|
208
|
+
# whose arguments mention "litestream replicate" is not mistaken for it.
|
|
209
|
+
def ps_process_info
|
|
210
|
+
output = `ps -axo pid=,stat=,lstart=,comm=`
|
|
211
|
+
return unless $?.success?
|
|
212
|
+
|
|
213
|
+
output.each_line do |line|
|
|
214
|
+
# lstart is always five words ("Mon Jul 1 11:10:58 2024"); comm is last
|
|
215
|
+
# because it may contain spaces.
|
|
216
|
+
pid, state, *lstart, command = line.strip.split(" ", 8)
|
|
217
|
+
next unless File.basename(command.to_s) == "litestream"
|
|
218
|
+
next unless `ps -o args= -p #{pid}`.match?(/litestream replicate(?: |$)/)
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
pid: pid,
|
|
222
|
+
status: PROCESS_STATES[state[0]],
|
|
223
|
+
started: Time.strptime(lstart.join(" "), "%a %b %d %H:%M:%S %Y").to_datetime
|
|
224
|
+
}
|
|
225
|
+
end
|
|
226
|
+
nil
|
|
227
|
+
rescue Errno::ENOENT
|
|
228
|
+
nil
|
|
190
229
|
end
|
|
191
230
|
end
|
|
192
231
|
end
|
|
@@ -2,17 +2,13 @@ require "puma/plugin"
|
|
|
2
2
|
|
|
3
3
|
# Copied from https://github.com/rails/solid_queue/blob/15408647f1780033dad223d3198761ea2e1e983e/lib/puma/plugin/solid_queue.rb
|
|
4
4
|
Puma::Plugin.create do
|
|
5
|
-
attr_reader :
|
|
5
|
+
attr_reader :litestream_pid, :log_writer
|
|
6
6
|
|
|
7
7
|
def start(launcher)
|
|
8
8
|
@log_writer = launcher.log_writer
|
|
9
|
-
@puma_pid = $$
|
|
10
9
|
|
|
11
10
|
launcher.events.on_booted do
|
|
12
|
-
@litestream_pid =
|
|
13
|
-
Thread.new { monitor_puma }
|
|
14
|
-
Litestream::Commands.replicate(async: true)
|
|
15
|
-
end
|
|
11
|
+
@litestream_pid = Litestream::Commands.replicate(async: true)
|
|
16
12
|
|
|
17
13
|
in_background do
|
|
18
14
|
monitor_litestream
|
|
@@ -33,10 +29,6 @@ Puma::Plugin.create do
|
|
|
33
29
|
rescue Errno::ECHILD, Errno::ESRCH
|
|
34
30
|
end
|
|
35
31
|
|
|
36
|
-
def monitor_puma
|
|
37
|
-
monitor(:puma_dead?, "Detected Puma has gone away, stopping Litestream...")
|
|
38
|
-
end
|
|
39
|
-
|
|
40
32
|
def monitor_litestream
|
|
41
33
|
monitor(:litestream_dead?, "Detected Litestream has gone away, stopping Puma...")
|
|
42
34
|
end
|
|
@@ -59,10 +51,6 @@ Puma::Plugin.create do
|
|
|
59
51
|
true
|
|
60
52
|
end
|
|
61
53
|
|
|
62
|
-
def puma_dead?
|
|
63
|
-
Process.ppid != puma_pid
|
|
64
|
-
end
|
|
65
|
-
|
|
66
54
|
def log(...)
|
|
67
55
|
log_writer.log(...)
|
|
68
56
|
end
|
|
@@ -32,40 +32,57 @@ namespace :litestream do
|
|
|
32
32
|
puts Litestream::Commands::Output.format(Litestream::Commands.databases(**options))
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
desc "List all
|
|
36
|
-
task
|
|
35
|
+
desc "List all LTX files for a database or replica, for example `rake litestream:ltx -- -database=storage/production.sqlite3`"
|
|
36
|
+
task ltx: :environment do
|
|
37
37
|
options = parse_argv_options
|
|
38
38
|
database = options.delete(:"--database") || options.delete(:"-database")
|
|
39
39
|
|
|
40
|
-
puts Litestream::Commands::Output.format(Litestream::Commands.
|
|
40
|
+
puts Litestream::Commands::Output.format(Litestream::Commands.ltx(database, **options))
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
desc "
|
|
44
|
-
task
|
|
43
|
+
desc "Show replication status, for example `rake litestream:status -- -database=storage/production.sqlite3`"
|
|
44
|
+
task status: :environment do
|
|
45
45
|
options = parse_argv_options
|
|
46
46
|
database = options.delete(:"--database") || options.delete(:"-database")
|
|
47
47
|
|
|
48
|
-
puts Litestream::Commands::Output.format(Litestream::Commands.
|
|
48
|
+
puts Litestream::Commands::Output.format(Litestream::Commands.status(database, **options))
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
-
desc "
|
|
52
|
-
task
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
desc "Explain the removal of the generations command"
|
|
52
|
+
task generations: :environment do
|
|
53
|
+
abort "`generations` was removed in Litestream 0.5; use `ltx` (see README, Upgrading from 0.3)"
|
|
54
|
+
end
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
)
|
|
56
|
+
desc "Explain the removal of the snapshots command"
|
|
57
|
+
task snapshots: :environment do
|
|
58
|
+
abort "`snapshots` was removed in Litestream 0.5; use `ltx` (see README, Upgrading from 0.3)"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
desc "Explain the removal of the wal command"
|
|
62
|
+
task wal: :environment do
|
|
63
|
+
abort "`wal` was removed in Litestream 0.5; use `ltx` (see README, Upgrading from 0.3)"
|
|
59
64
|
end
|
|
60
65
|
|
|
61
66
|
private
|
|
62
67
|
|
|
68
|
+
# Options after `--` come as `-flag`, `-flag=value` or `-flag value`. Rake
|
|
69
|
+
# also reads a bare word there as one more task to run, so each value gets
|
|
70
|
+
# an empty task of its own to keep Rake from aborting once this one is done.
|
|
63
71
|
def parse_argv_options
|
|
64
72
|
options = {}
|
|
65
73
|
if (separator_index = ARGV.index("--"))
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
.
|
|
74
|
+
awaiting_value = nil
|
|
75
|
+
ARGV.drop(separator_index + 1).each do |arg|
|
|
76
|
+
if arg.start_with?("-")
|
|
77
|
+
key, value = arg.split("=", 2)
|
|
78
|
+
options[key] = value
|
|
79
|
+
awaiting_value = value.nil? ? key : nil
|
|
80
|
+
elsif awaiting_value
|
|
81
|
+
options[awaiting_value] = arg
|
|
82
|
+
awaiting_value = nil
|
|
83
|
+
Rake::Task.define_task(arg)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
69
86
|
end
|
|
70
87
|
options.symbolize_keys!
|
|
71
88
|
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: litestream
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.16.0
|
|
5
5
|
platform: x86_64-linux
|
|
6
6
|
authors:
|
|
7
7
|
- Stephen Margheim
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: sqlite3
|
|
@@ -23,6 +23,7 @@ dependencies:
|
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '0'
|
|
26
|
+
force_ruby_platform: false
|
|
26
27
|
- !ruby/object:Gem::Dependency
|
|
27
28
|
name: actionpack
|
|
28
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -37,6 +38,7 @@ dependencies:
|
|
|
37
38
|
- - ">="
|
|
38
39
|
- !ruby/object:Gem::Version
|
|
39
40
|
version: '7.0'
|
|
41
|
+
force_ruby_platform: false
|
|
40
42
|
- !ruby/object:Gem::Dependency
|
|
41
43
|
name: actionview
|
|
42
44
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -51,6 +53,7 @@ dependencies:
|
|
|
51
53
|
- - ">="
|
|
52
54
|
- !ruby/object:Gem::Version
|
|
53
55
|
version: '7.0'
|
|
56
|
+
force_ruby_platform: false
|
|
54
57
|
- !ruby/object:Gem::Dependency
|
|
55
58
|
name: activejob
|
|
56
59
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -65,6 +68,7 @@ dependencies:
|
|
|
65
68
|
- - ">="
|
|
66
69
|
- !ruby/object:Gem::Version
|
|
67
70
|
version: '7.0'
|
|
71
|
+
force_ruby_platform: false
|
|
68
72
|
- !ruby/object:Gem::Dependency
|
|
69
73
|
name: activesupport
|
|
70
74
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -79,6 +83,7 @@ dependencies:
|
|
|
79
83
|
- - ">="
|
|
80
84
|
- !ruby/object:Gem::Version
|
|
81
85
|
version: '7.0'
|
|
86
|
+
force_ruby_platform: false
|
|
82
87
|
- !ruby/object:Gem::Dependency
|
|
83
88
|
name: railties
|
|
84
89
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -93,6 +98,7 @@ dependencies:
|
|
|
93
98
|
- - ">="
|
|
94
99
|
- !ruby/object:Gem::Version
|
|
95
100
|
version: '7.0'
|
|
101
|
+
force_ruby_platform: false
|
|
96
102
|
- !ruby/object:Gem::Dependency
|
|
97
103
|
name: rails
|
|
98
104
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -107,20 +113,6 @@ dependencies:
|
|
|
107
113
|
- - ">="
|
|
108
114
|
- !ruby/object:Gem::Version
|
|
109
115
|
version: '0'
|
|
110
|
-
- !ruby/object:Gem::Dependency
|
|
111
|
-
name: rubyzip
|
|
112
|
-
requirement: !ruby/object:Gem::Requirement
|
|
113
|
-
requirements:
|
|
114
|
-
- - ">="
|
|
115
|
-
- !ruby/object:Gem::Version
|
|
116
|
-
version: '0'
|
|
117
|
-
type: :development
|
|
118
|
-
prerelease: false
|
|
119
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
-
requirements:
|
|
121
|
-
- - ">="
|
|
122
|
-
- !ruby/object:Gem::Version
|
|
123
|
-
version: '0'
|
|
124
116
|
email:
|
|
125
117
|
- stephen.margheim@gmail.com
|
|
126
118
|
executables:
|
|
@@ -142,6 +134,7 @@ files:
|
|
|
142
134
|
- config/routes.rb
|
|
143
135
|
- exe/litestream
|
|
144
136
|
- exe/x86_64-linux/litestream
|
|
137
|
+
- exe/x86_64-linux/litestream.sha256
|
|
145
138
|
- lib/litestream.rb
|
|
146
139
|
- lib/litestream/commands.rb
|
|
147
140
|
- lib/litestream/engine.rb
|
|
@@ -174,7 +167,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
174
167
|
- !ruby/object:Gem::Version
|
|
175
168
|
version: '0'
|
|
176
169
|
requirements: []
|
|
177
|
-
rubygems_version: 3.6.
|
|
170
|
+
rubygems_version: 3.6.7
|
|
178
171
|
specification_version: 4
|
|
179
172
|
summary: Integrate Litestream with the RubyGems infrastructure.
|
|
180
173
|
test_files: []
|