railwatch 0.3.4 → 0.3.6

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.
Files changed (181) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -2
  3. data/app/channels/railwatch/environment_channel.rb +17 -4
  4. data/app/jobs/railwatch/prune_telemetry_job.rb +20 -1
  5. data/app/models/railwatch/ingest/batch.rb +1 -1
  6. data/app/models/railwatch/ingest/mapper.rb +1 -3
  7. data/app/models/railwatch/telemetry_record.rb +48 -0
  8. data/db/railwatch_telemetry_migrate/20260903000000_enable_incremental_vacuum.rb +62 -0
  9. data/docs/embedded.md +64 -7
  10. data/docs/getting-started.md +0 -2
  11. data/docs/source-maps.md +1 -3
  12. data/lib/generators/railwatch/install/install_generator.rb +0 -8
  13. data/lib/railwatch/backtrace.rb +12 -2
  14. data/lib/railwatch/configuration.rb +6 -5
  15. data/lib/railwatch/engine.rb +2 -2
  16. data/lib/railwatch/export/client.rb +10 -3
  17. data/lib/railwatch/reporter.rb +2 -12
  18. data/lib/railwatch/source_maps.rb +13 -1
  19. data/lib/railwatch/spec_helper.rb +1 -1
  20. data/lib/railwatch/transport/http.rb +14 -5
  21. data/lib/railwatch/version.rb +1 -1
  22. data/lib/tasks/railwatch_tasks.rake +161 -0
  23. data/public/railwatch/assets/{app-layout-yh-sPWgK.js → app-layout-B5Fcdwfz.js} +1 -1
  24. data/public/railwatch/assets/{app-wordmark-nbjkzxwQ.js → app-wordmark-1SRllgHv.js} +1 -1
  25. data/public/railwatch/assets/{appearance-CDuRvQTB.js → appearance-DbwahDrc.js} +1 -1
  26. data/public/railwatch/assets/application-CwshqwM6.css +1 -0
  27. data/public/railwatch/assets/{arrow-up-DVtOGdVA.js → arrow-up-DwKQcab4.js} +1 -1
  28. data/public/railwatch/assets/{auth-layout-TCwPpS1K.js → auth-layout-DXfxXAN_.js} +1 -1
  29. data/public/railwatch/assets/{badge-Daw4Hvr8.js → badge-CApjyWcg.js} +1 -1
  30. data/public/railwatch/assets/{braces-DhFbHPsz.js → braces-kGSgxWnS.js} +1 -1
  31. data/public/railwatch/assets/{card-BX_3HXcJ.js → card-BaJ8kNkp.js} +1 -1
  32. data/public/railwatch/assets/{chart-B14-N9g7.js → chart-Xnrz1Nqw.js} +1 -1
  33. data/public/railwatch/assets/{chart-hover-Vy52H4uD.js → chart-hover-XYPNE2CX.js} +1 -1
  34. data/public/railwatch/assets/{chart-panel-Cb4S_ej_.js → chart-panel-BYk68mJJ.js} +1 -1
  35. data/public/railwatch/assets/{checkbox-D3aRSsBj.js → checkbox-Bf41R8W6.js} +1 -1
  36. data/public/railwatch/assets/{code-KvW8k7Jr.js → code-a-9sh36A.js} +1 -1
  37. data/public/railwatch/assets/{copy-block-BKFGd11J.js → copy-block-zykZWQym.js} +1 -1
  38. data/public/railwatch/assets/{copy-id-Djks1fXB.js → copy-id-b6CW5ESL.js} +1 -1
  39. data/public/railwatch/assets/{cursor-load-more-BXV0f1D_.js → cursor-load-more-0cHzbTPT.js} +1 -1
  40. data/public/railwatch/assets/{data-table-iv1bdF6u.js → data-table-IvA7xvrK.js} +1 -1
  41. data/public/railwatch/assets/{edit-BZc_Iawe.js → edit-7lmf3k80.js} +1 -1
  42. data/public/railwatch/assets/{edit-__9yJlO3.js → edit-DTtRV1fO.js} +1 -1
  43. data/public/railwatch/assets/{edit-DMKUF8Zi.js → edit-ZKE15r-F.js} +1 -1
  44. data/public/railwatch/assets/{empty-state-6j_0AaQQ.js → empty-state-BFTnQ2oT.js} +1 -1
  45. data/public/railwatch/assets/env-layout-erSDpIXg.js +1 -0
  46. data/public/railwatch/assets/{execution-path-CzgBUi5e.js → execution-path-Di5uUHi-.js} +1 -1
  47. data/public/railwatch/assets/{filter-bar-9SU5NrzX.js → filter-bar-Btm__gQi.js} +1 -1
  48. data/public/railwatch/assets/{flamegraph-qcekju8V.js → flamegraph-C-mZdqa7.js} +1 -1
  49. data/public/railwatch/assets/{frames-Cyu7KMxZ.js → frames-KZBsjg6-.js} +1 -1
  50. data/public/railwatch/assets/{google-sign-in-button-DsTSfmzY.js → google-sign-in-button-DJRoD72g.js} +1 -1
  51. data/public/railwatch/assets/index-9LL_C3eG.js +1 -0
  52. data/public/railwatch/assets/index-B-bSm1jB.js +1 -0
  53. data/public/railwatch/assets/{index-OqI59zPb.js → index-B0VXzXYG.js} +1 -1
  54. data/public/railwatch/assets/index-BAkZNNEh.js +1 -0
  55. data/public/railwatch/assets/{index-Cmlwy1-V.js → index-BWSyQRZI.js} +1 -1
  56. data/public/railwatch/assets/index-Bjw4sTXU.js +1 -0
  57. data/public/railwatch/assets/index-Bo8hrjbs.js +1 -0
  58. data/public/railwatch/assets/index-BoTOVM0v.js +1 -0
  59. data/public/railwatch/assets/index-Bot7kHMA.js +1 -0
  60. data/public/railwatch/assets/index-BsrCnQqg.js +1 -0
  61. data/public/railwatch/assets/index-BtSlLmkg.js +1 -0
  62. data/public/railwatch/assets/{index-BRiq3SNR.js → index-CAzQkw-h.js} +1 -1
  63. data/public/railwatch/assets/index-CBJZQgHA.js +1 -0
  64. data/public/railwatch/assets/index-CIUl9POV.js +1 -0
  65. data/public/railwatch/assets/{index-DlumCsWY.js → index-CP6jrrPT.js} +2 -2
  66. data/public/railwatch/assets/index-CU6yhEyt.js +1 -0
  67. data/public/railwatch/assets/{index-1ol1-QWI.js → index-CX8Br7m9.js} +1 -1
  68. data/public/railwatch/assets/index-CYYmZpZu.js +1 -0
  69. data/public/railwatch/assets/{index-DU9F5b5d.js → index-CeqMDByC.js} +1 -1
  70. data/public/railwatch/assets/index-Cj3_jD1E.js +1 -0
  71. data/public/railwatch/assets/index-D4968-TC.js +1 -0
  72. data/public/railwatch/assets/index-DAEzO_vq.js +1 -0
  73. data/public/railwatch/assets/index-DBTxQw77.js +1 -0
  74. data/public/railwatch/assets/index-DCAho97K.js +1 -0
  75. data/public/railwatch/assets/index-DEfu_YvU.js +1 -0
  76. data/public/railwatch/assets/{index-CeYKnapu.js → index-DXUwhSCQ.js} +1 -1
  77. data/public/railwatch/assets/index-DcItJxa5.js +1 -0
  78. data/public/railwatch/assets/index-EGU__DOB.js +1 -0
  79. data/public/railwatch/assets/index-HCJmFcZu.js +1 -0
  80. data/public/railwatch/assets/index-P_e_cI0O.js +1 -0
  81. data/public/railwatch/assets/index-QyHE9WsU.js +1 -0
  82. data/public/railwatch/assets/index-Z1c4nFL0.js +1 -0
  83. data/public/railwatch/assets/index-kNhZPLVj.js +1 -0
  84. data/public/railwatch/assets/index-uHBVO8Lh.js +1 -0
  85. data/public/railwatch/assets/index-wOj5j0KC.js +1 -0
  86. data/public/railwatch/assets/{inertia-TViv6kNv.js → inertia-Degv_G9N.js} +2 -2
  87. data/public/railwatch/assets/{input-error-LxImUkxv.js → input-error-C6SqO_iU.js} +1 -1
  88. data/public/railwatch/assets/{json-viewer-Ar4cjPDW.js → json-viewer-DcQIxT86.js} +1 -1
  89. data/public/railwatch/assets/klass-WgQEfWpo.js +1 -0
  90. data/public/railwatch/assets/{label-COUKWqE_.js → label-B_vK19tR.js} +1 -1
  91. data/public/railwatch/assets/{layout-DNSLAkw_.js → layout-C8q7GUlZ.js} +1 -1
  92. data/public/railwatch/assets/{live-dot-ChfUtY3p.js → live-dot-DaXEbkXo.js} +1 -1
  93. data/public/railwatch/assets/{nav-CNnDqPlm.js → nav-BnD5XArZ.js} +1 -1
  94. data/public/railwatch/assets/{new-Be55nmt9.js → new-BL9OqXHe.js} +1 -1
  95. data/public/railwatch/assets/{new-D05SajFR.js → new-BhRnQn4w.js} +1 -1
  96. data/public/railwatch/assets/{new-Bi_xQiIb.js → new-BqP0NhOF.js} +1 -1
  97. data/public/railwatch/assets/{new-BvCT8TMg.js → new-CWP3wtwt.js} +1 -1
  98. data/public/railwatch/assets/{new-D4uewYC8.js → new-DIjwu2jF.js} +1 -1
  99. data/public/railwatch/assets/{new-84S8ZJq9.js → new-uxMnBtoY.js} +1 -1
  100. data/public/railwatch/assets/{onboarding-CYZi5Cqc.js → onboarding-C_Y3xTsH.js} +1 -1
  101. data/public/railwatch/assets/{origin-identity-6q1-CBts.js → origin-identity-CNhOnfBa.js} +1 -1
  102. data/public/railwatch/assets/{percentile-picker-gFZCXtdb.js → percentile-picker-BujVEhP6.js} +1 -1
  103. data/public/railwatch/assets/{relative-time-IOOgl5n2.js → relative-time-6CoKr1lN.js} +1 -1
  104. data/public/railwatch/assets/release-health-1Qzc_XhT.js +1 -0
  105. data/public/railwatch/assets/route-xqgYjOwJ.js +1 -0
  106. data/public/railwatch/assets/{segmented-h1VdDTqE.js → segmented-CVKq2Xpa.js} +1 -1
  107. data/public/railwatch/assets/{select-_AJsUa7X.js → select-CsmSXsJ4.js} +1 -1
  108. data/public/railwatch/assets/{separator-BwwTYtCF.js → separator-CrM10Oot.js} +1 -1
  109. data/public/railwatch/assets/{series-chart-DaFPefku.js → series-chart-D33UhdG2.js} +1 -1
  110. data/public/railwatch/assets/{show-C8sHalnw.js → show-8H9WAGav.js} +1 -1
  111. data/public/railwatch/assets/show-9jDeBNpc.js +1 -0
  112. data/public/railwatch/assets/{show-DACku6AD.js → show-B0nHy31L.js} +3 -3
  113. data/public/railwatch/assets/{show-C4Ltf5i9.js → show-B3izvRdU.js} +1 -1
  114. data/public/railwatch/assets/show-B7NhcWgp.js +1 -0
  115. data/public/railwatch/assets/show-BM-E2iYc.js +1 -0
  116. data/public/railwatch/assets/show-BlNR3ZOZ.js +1 -0
  117. data/public/railwatch/assets/{show-DgSIoRvA.js → show-CJMeX75E.js} +1 -1
  118. data/public/railwatch/assets/{show-CeTL4B37.js → show-CMisHNAI.js} +2 -2
  119. data/public/railwatch/assets/{show-JxFtB4eK.js → show-CQ4Ze-Un.js} +2 -2
  120. data/public/railwatch/assets/{show-BM6X2Mpo.js → show-CvNV23gH.js} +1 -1
  121. data/public/railwatch/assets/show-DMZlwzL_.js +1 -0
  122. data/public/railwatch/assets/show-DUwQlohF.js +1 -0
  123. data/public/railwatch/assets/{show-DIOSGcXV.js → show-DZwcZZrz.js} +2 -2
  124. data/public/railwatch/assets/show-ixK2r2f1.js +1 -0
  125. data/public/railwatch/assets/{show-DYteoYWW.js → show-rysBbvt-.js} +1 -1
  126. data/public/railwatch/assets/show-zxVxhUel.js +1 -0
  127. data/public/railwatch/assets/{sort-header-DpFzXblu.js → sort-header-CKj_7G3U.js} +1 -1
  128. data/public/railwatch/assets/{sparkline-cell-C3-5vFkP.js → sparkline-cell-Ckg29yVq.js} +1 -1
  129. data/public/railwatch/assets/stat-TxvoBiC8.js +1 -0
  130. data/public/railwatch/assets/{status-badge-8jVV-LA4.js → status-badge-5NCVJHjo.js} +1 -1
  131. data/public/railwatch/assets/{tenant-path-G-6u9A-o.js → tenant-path-B5ZRK4x3.js} +1 -1
  132. data/public/railwatch/assets/{text-link-DfsiaCcP.js → text-link-BPllRj0F.js} +1 -1
  133. data/public/railwatch/assets/{textarea-Dye72uP7.js → textarea-DtD5yZp_.js} +1 -1
  134. data/public/railwatch/assets/{timeline-CD7WHnbo.js → timeline-DZz5ZnHN.js} +1 -1
  135. data/public/railwatch/assets/{transition-B_AW8rMK.js → transition-Ds6Cakuc.js} +1 -1
  136. data/public/railwatch/assets/{use-clipboard-ColgLyQ2.js → use-clipboard-BLjjYH8V.js} +1 -1
  137. data/public/railwatch/assets/use-live-GKze1mNC.js +1 -0
  138. data/public/railwatch/manifest.json +1291 -1238
  139. metadata +117 -115
  140. data/public/railwatch/assets/application-DD4nJA5I.css +0 -1
  141. data/public/railwatch/assets/env-layout-DrT8rO6P.js +0 -1
  142. data/public/railwatch/assets/index-5jI4aFzC.js +0 -1
  143. data/public/railwatch/assets/index-9KTrVnrc.js +0 -1
  144. data/public/railwatch/assets/index-B0-8lcTp.js +0 -1
  145. data/public/railwatch/assets/index-B7jjfNfO.js +0 -1
  146. data/public/railwatch/assets/index-BBchRy0M.js +0 -1
  147. data/public/railwatch/assets/index-BgKj9xhr.js +0 -1
  148. data/public/railwatch/assets/index-BkTZqqOu.js +0 -1
  149. data/public/railwatch/assets/index-BprKx8QO.js +0 -1
  150. data/public/railwatch/assets/index-C3jzvPs3.js +0 -1
  151. data/public/railwatch/assets/index-Cbs6gGyQ.js +0 -1
  152. data/public/railwatch/assets/index-CdRZ6AWF.js +0 -1
  153. data/public/railwatch/assets/index-Cwx6058d.js +0 -1
  154. data/public/railwatch/assets/index-D4CSdbHv.js +0 -1
  155. data/public/railwatch/assets/index-DEFMSkdG.js +0 -1
  156. data/public/railwatch/assets/index-DFiHEBSh.js +0 -1
  157. data/public/railwatch/assets/index-DL4vWdWJ.js +0 -1
  158. data/public/railwatch/assets/index-DaXgPcGL.js +0 -1
  159. data/public/railwatch/assets/index-DbtaU-EE.js +0 -1
  160. data/public/railwatch/assets/index-DeOe83F4.js +0 -1
  161. data/public/railwatch/assets/index-DiucHN4B.js +0 -1
  162. data/public/railwatch/assets/index-DlnR_l9o.js +0 -1
  163. data/public/railwatch/assets/index-DmRd7aIG.js +0 -1
  164. data/public/railwatch/assets/index-DxSh2UpM.js +0 -1
  165. data/public/railwatch/assets/index-MIMGuFNt.js +0 -1
  166. data/public/railwatch/assets/index-P4rC7IlX.js +0 -1
  167. data/public/railwatch/assets/index-gpPOcFWq.js +0 -1
  168. data/public/railwatch/assets/index-oVkururr.js +0 -1
  169. data/public/railwatch/assets/index-p9puqVge.js +0 -1
  170. data/public/railwatch/assets/klass-CJ-J4INB.js +0 -1
  171. data/public/railwatch/assets/release-health-DC8oc7uw.js +0 -1
  172. data/public/railwatch/assets/route-Dv6LAWvT.js +0 -1
  173. data/public/railwatch/assets/show-B7NCgkEo.js +0 -1
  174. data/public/railwatch/assets/show-BKqyKjBK.js +0 -1
  175. data/public/railwatch/assets/show-BNw4tN5q.js +0 -1
  176. data/public/railwatch/assets/show-BO3bnG5h.js +0 -1
  177. data/public/railwatch/assets/show-BhrAVAEA.js +0 -1
  178. data/public/railwatch/assets/show-CpfgV1jP.js +0 -1
  179. data/public/railwatch/assets/show-DQp_1n-B.js +0 -1
  180. data/public/railwatch/assets/show-DVNz46RI.js +0 -1
  181. data/public/railwatch/assets/stat-s4RpOS9w.js +0 -1
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Railwatch
4
- VERSION = "0.3.4"
4
+ VERSION = "0.3.6"
5
5
  end
@@ -28,6 +28,130 @@ module Railwatch
28
28
  end
29
29
  end
30
30
 
31
+ # What the embedded telemetry database is costing on disk, and the one-time
32
+ # conversion that lets pruning give that cost back.
33
+ #
34
+ # Deleting rows does not shrink a SQLite file. The pages go on the freelist
35
+ # and are reused by later inserts, and the only way back to the filesystem
36
+ # is auto_vacuum=incremental plus PRAGMA incremental_vacuum -- which
37
+ # PruneTelemetryJob now runs after every prune. A database can only enter
38
+ # that mode while it is still empty (EnableIncrementalVacuum does that for
39
+ # every telemetry database created since it shipped) or through a full
40
+ # VACUUM, which is what this offers to an older one.
41
+ class TelemetryDisk
42
+ # Deliberately pessimistic, and said out loud as an estimate: VACUUM
43
+ # rewrites the whole file, and what that costs is the operator's disk,
44
+ # not ours to know.
45
+ VACUUM_BYTES_PER_SECOND = 50 * 1024 * 1024
46
+
47
+ # nil, having said why, for an install with no telemetry database to
48
+ # talk about.
49
+ def self.open
50
+ unless Railwatch.config.local?
51
+ puts "Railwatch reports over HTTP here (transport = :http), so this app has no telemetry database; nothing to vacuum."
52
+ return nil
53
+ end
54
+
55
+ environment = Railwatch::Environment.current
56
+ return new(environment) if environment.with_telemetry { Railwatch::TelemetryRecord.sqlite? }
57
+
58
+ puts "The railwatch_telemetry database is not SQLite; auto_vacuum does not apply."
59
+ nil
60
+ end
61
+
62
+ def initialize(environment)
63
+ @environment = environment
64
+ end
65
+
66
+ def report
67
+ with_telemetry do
68
+ <<~TEXT.chomp
69
+ Railwatch telemetry database
70
+ file #{path}
71
+ size #{human(file_bytes)}#{" + #{human(wal_bytes)} WAL" if wal_bytes.positive?}
72
+ mode auto_vacuum=#{mode}
73
+ free #{freelist} pages (#{human(freelist * page_size)}) on the freelist
74
+ TEXT
75
+ end
76
+ end
77
+
78
+ def advice
79
+ with_telemetry do
80
+ if mode != :incremental
81
+ "\nPruning cannot return space to the filesystem in this mode: every page it frees stays in this file.\n" \
82
+ "`bin/rails railwatch:vacuum` converts the database to incremental auto-vacuum with a full VACUUM. " \
83
+ "That rewrites all #{human(file_bytes)}, needs about that much free disk for the temporary copy, and " \
84
+ "holds the write lock for roughly #{estimate}. Telemetry written while it runs waits for it."
85
+ elsif freelist.positive?
86
+ "\nThe nightly prune returns up to #{human(Railwatch::PruneTelemetryJob::VACUUM_PAGES_PER_SLICE * Railwatch::PruneTelemetryJob::VACUUM_SLICES * page_size)} " \
87
+ "a night on its own. `bin/rails railwatch:vacuum` returns all #{human(freelist * page_size)} now."
88
+ else
89
+ "\nNothing on the freelist: pruning is already returning this database's space as it goes."
90
+ end
91
+ end
92
+ end
93
+
94
+ # The whole point of asking explicitly, so this one is not bounded the way
95
+ # the nightly prune is.
96
+ def reclaim!
97
+ with_telemetry do
98
+ before = disk_bytes
99
+ if mode == :incremental
100
+ Railwatch::TelemetryRecord.incremental_vacuum
101
+ else
102
+ puts "\nConverting to incremental auto-vacuum (full VACUUM, roughly #{estimate})..."
103
+ connection.execute("PRAGMA auto_vacuum = incremental")
104
+ connection.execute("VACUUM")
105
+ end
106
+ # A checkpoint that cannot finish says so in its result rather than
107
+ # raising: a reader still on an older snapshot holds the WAL open.
108
+ # Counting only the main file would then report the WAL's bytes as
109
+ # returned while they are still on the disk.
110
+ busy = checkpoint_busy?
111
+ [
112
+ "\nauto_vacuum=#{mode}, #{human(disk_bytes)} on disk including the WAL " \
113
+ "(#{human(before - disk_bytes)} returned), #{freelist} pages left on the freelist.",
114
+ busy ? "The WAL could not be truncated yet -- something is still reading it. " \
115
+ "Its bytes come back at the next checkpoint; run this again if you want to watch it." : nil
116
+ ].compact.join("\n")
117
+ end
118
+ end
119
+
120
+ private
121
+ def with_telemetry(&) = @environment.with_telemetry(&)
122
+
123
+ def connection = Railwatch::TelemetryRecord.connection
124
+
125
+ def mode = Railwatch::TelemetryRecord.auto_vacuum_mode
126
+
127
+ def freelist = Railwatch::TelemetryRecord.freelist_pages
128
+
129
+ def page_size = Railwatch::TelemetryRecord.page_size
130
+
131
+ def path = Rails.root.join(Railwatch::TelemetryRecord.connection_db_config.database.to_s)
132
+
133
+ def file_bytes = File.exist?(path) ? File.size(path) : 0
134
+
135
+ # What this database actually occupies. In WAL mode the pages a VACUUM
136
+ # frees are not off the disk until the WAL is checkpointed, so the main
137
+ # file alone understates it -- and, right after a vacuum, flatters it.
138
+ def disk_bytes = file_bytes + wal_bytes
139
+
140
+ # `PRAGMA wal_checkpoint` answers with [busy, log_pages, checkpointed];
141
+ # a non-zero first column means it gave up rather than failed.
142
+ def checkpoint_busy?
143
+ connection.select_rows("PRAGMA wal_checkpoint(TRUNCATE)").dig(0, 0).to_i != 0
144
+ rescue StandardError
145
+ false
146
+ end
147
+
148
+ def wal_bytes = File.exist?("#{path}-wal") ? File.size("#{path}-wal") : 0
149
+
150
+ def estimate = ActiveSupport::Duration.build([ (file_bytes / VACUUM_BYTES_PER_SECOND.to_f).ceil, 1 ].max).inspect
151
+
152
+ def human(bytes) = ActiveSupport::NumberHelper.number_to_human_size(bytes)
153
+ end
154
+
31
155
  # Where this install's platform lives, derived from config.ingest_url:
32
156
  # railwatch:token and railwatch:mcp point at the same host the gem already
33
157
  # ships to, so a self-hosted app never gets told to visit railwatch.rebulk.com.
@@ -127,6 +251,25 @@ namespace :railwatch do
127
251
  else "cannot check: #{pending}"
128
252
  end, fatal: true)
129
253
  end
254
+ # An install created before 0.3.5 is in auto_vacuum=none, where the
255
+ # nightly prune's reclaim is a silent no-op and the file only grows.
256
+ # Nothing converts it on its own, because that needs a full VACUUM with
257
+ # the write lock held -- so the check people actually run is where it
258
+ # has to be said, rather than leaving them to notice the disk.
259
+ if Railwatch.config.local? && Railwatch::TelemetryRecord.sqlite?
260
+ vacuum_mode = begin
261
+ Railwatch::Environment.current.with_telemetry { Railwatch::TelemetryRecord.auto_vacuum_mode }
262
+ rescue StandardError => e
263
+ e.message
264
+ end
265
+ check.call(vacuum_mode == :incremental, "telemetry disk",
266
+ case vacuum_mode
267
+ when :incremental then "auto_vacuum=incremental; the nightly prune returns freed pages"
268
+ when Symbol then "auto_vacuum=#{vacuum_mode}: pruned pages stay in the file " \
269
+ "(bin/rails railwatch:vacuum:status)"
270
+ else "cannot check: #{vacuum_mode}"
271
+ end)
272
+ end
130
273
  # The maintenance clock runs in web and worker processes, not in this
131
274
  # rake process, so what can be checked here is whether one has ticked.
132
275
  last_tick = begin
@@ -297,6 +440,24 @@ namespace :railwatch do
297
440
  puts "\nRailwatch is wired up."
298
441
  end
299
442
 
443
+ # Disk. `railwatch:vacuum:status` only reads; `railwatch:vacuum` is the
444
+ # one-time conversion a database created before EnableIncrementalVacuum
445
+ # needs, and it is a rake task rather than anything automatic because it
446
+ # rewrites the whole file with the write lock held.
447
+ desc "Report the telemetry database's size, free pages, and whether pruning can return them to the filesystem"
448
+ task "vacuum:status" => :environment do
449
+ disk = Railwatch::TelemetryDisk.open or next
450
+ puts disk.report
451
+ puts disk.advice
452
+ end
453
+
454
+ desc "Reclaim disk from the telemetry database. Converts it to incremental auto-vacuum if needed -- a full VACUUM, which locks the file"
455
+ task vacuum: :environment do
456
+ disk = Railwatch::TelemetryDisk.open or next
457
+ puts disk.report
458
+ puts disk.reclaim!
459
+ end
460
+
300
461
  namespace :export do
301
462
  desc "Show what the export queue is holding and whether it can send"
302
463
  task status: :environment do