Skip to content

[HEAVILY WIP] Migrate to ClickHouse#1470

Open
skyfallwastaken wants to merge 14 commits into
mainfrom
clickhouse
Open

[HEAVILY WIP] Migrate to ClickHouse#1470
skyfallwastaken wants to merge 14 commits into
mainfrom
clickhouse

Conversation

@skyfallwastaken

Copy link
Copy Markdown
Member

Warning

This PR is incomplete.

Summary of the problem

  • Postgres slow :(
  • ~500GB of storage used for heartbeats alone. This is woefully inefficient.

Describe your changes

  • Removes dashboard rollups. They caused a ton of DB queries and ClickHouse is fast enough to not need them anyway.
  • Migrates all queries to ClickHouse.

Not in this PR but there's a PeerDB mirror from Postgres -> CH atm. It'll get turned off once we move

Screenshots / Media

N/A

Copilot AI review requested due to automatic review settings July 8, 2026 19:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread app/controllers/admin/account_merger_controller.rb Fixed
Comment thread app/controllers/admin/account_merger_controller.rb Fixed
@socket-security

socket-security Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedgem/​clickhouse-native@​0.10.096100100100100
Addedgem/​clickhouse-activerecord@​1.6.799100100100100

View full report

@greptile-apps

greptile-apps Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR migrates Hackatime heartbeat storage and stats reads from Postgres rollups to ClickHouse. The main changes are:

  • Adds ClickHouse database config, migrations, CI services, and client gems.
  • Moves heartbeat ingestion, soft deletes, account merges, and imports onto ClickHouse write paths.
  • Adds serving-table aggregates and readers for faster user, project, and dimension stats.
  • Updates controllers, jobs, services, and tests to read heartbeat activity from ClickHouse.

Confidence Score: 3/5

This PR is not ready to merge until CI can reach ClickHouse through the native client.

The migration touches core heartbeat ingest and read paths, and the CI ClickHouse service configuration currently blocks tests that exercise the new serving reader.

.github/workflows/ci.yml; app/models/clickhouse/heartbeat.rb; app/models/clickhouse/heartbeat_writer.rb

T-Rex T-Rex Logs

What T-Rex did

  • I reproduced the unit-test CI port exposure issue by running a narrow Ruby repro script, which showed that only 8123:8123 is exposed and that ServingRepository maps the native port to 9000, causing a failure because 9000 is not exposed.
  • I reproduced the system-test CI port exposure issue with a generated Python repro script, which confirmed only 8123:8123 is published and that CLICKHOUSE_NATIVE_PORT is unset, leading ServingRepository to resolve to port 9000 and fail due to missing 9000:9000 mapping.
  • I inspected the test environment preconditions and Bundler setup, finding that the pre-test logs show Bundler and Rails tooling were not properly configured (absent CLI paths for docker/psql/clickhouse and a failing bin/rails -v) and that the after-run log repeats the Bundler parse failure with EXIT_CODE 1.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
.github/workflows/ci.yml Adds ClickHouse CI services, but omits native TCP port exposure required by the new serving-table client.
app/models/clickhouse/serving_repository.rb Adds native ClickHouse serving-table read repository with explicit SQL construction and date-range corrections.
app/models/clickhouse/heartbeat.rb Migrates heartbeat read helpers to ClickHouse with duration, grouping, and span helpers.
app/services/heartbeat_ingest.rb Switches direct/import ingestion to shape rows and insert into ClickHouse, including in-batch dedup and serving-table maintenance.
app/models/clickhouse/heartbeat_writer.rb Introduces ClickHouse heartbeat writes, soft deletes, account merge writes, and serving-table rebuild hooks.
app/controllers/api/v1/stats_controller.rb Moves public stats endpoints to ClickHouse and serving-table reads while preserving fallback paths for unsupported filters.
app/services/heartbeat_intervals/user_rebuilder.rb Adds per-user serving-table rebuild logic that writes net correction deltas from canonical ClickHouse heartbeats.
db/clickhouse/20260710000001_create_heartbeat_serving_tables.rb Creates initial ClickHouse serving delta and aggregate tables for heartbeat durations by user, project, and dimensions.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
  participant Client
  participant Rails as Rails API/Jobs
  participant Writer as Clickhouse::HeartbeatWriter
  participant CH as ClickHouse heartbeats
  participant Delta as HeartbeatIntervals
  participant Serving as Serving tables
  participant Reader as Clickhouse::StatsReader

  Client->>Rails: submit/query heartbeats
  Rails->>Writer: insert_rows / merge / soft_delete
  Writer->>CH: write heartbeat versions
  Writer->>Delta: emit deltas or enqueue rebuild
  Delta->>Serving: aggregate daily/user/project/dimension stats
  Rails->>Reader: total_seconds/project_durations/filter_durations
  Reader->>Serving: native ClickHouse queries
  Serving-->>Rails: pre-aggregated stats
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
  participant Client
  participant Rails as Rails API/Jobs
  participant Writer as Clickhouse::HeartbeatWriter
  participant CH as ClickHouse heartbeats
  participant Delta as HeartbeatIntervals
  participant Serving as Serving tables
  participant Reader as Clickhouse::StatsReader

  Client->>Rails: submit/query heartbeats
  Rails->>Writer: insert_rows / merge / soft_delete
  Writer->>CH: write heartbeat versions
  Writer->>Delta: emit deltas or enqueue rebuild
  Delta->>Serving: aggregate daily/user/project/dimension stats
  Rails->>Reader: total_seconds/project_durations/filter_durations
  Reader->>Serving: native ClickHouse queries
  Serving-->>Rails: pre-aggregated stats
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
.github/workflows/ci.yml:155
**Expose native port**
The test job only publishes ClickHouse HTTP port `8123`, but the new `Clickhouse::ServingRepository` uses `clickhouse-native`, whose native protocol connects to TCP port `9000` by default. Any test path that reaches `StatsReader` or serving-table reads will try `localhost:9000` and fail in CI because that port is not mapped from the service container.

### Issue 2 of 2
.github/workflows/ci.yml:223
**Expose native port**
The system-test ClickHouse service has the same issue as the unit-test job: only `8123` is exposed, while `Clickhouse::ServingRepository` talks through the native client on port `9000` unless `CLICKHOUSE_NATIVE_PORT` overrides it. System tests that render project or profile stats through `StatsReader` will fail to connect to ClickHouse in CI.

Reviews (4): Last reviewed commit: "oops, remove my scratch htmls" | Re-trigger Greptile

Comment thread app/models/clickhouse/heartbeat.rb Outdated
Comment thread app/models/clickhouse/heartbeat.rb Outdated
Comment thread app/models/clickhouse/heartbeat_writer.rb Fixed
Comment thread app/models/clickhouse/heartbeat_writer.rb Fixed
Comment thread app/models/clickhouse/heartbeat_writer.rb Fixed
@skyfallwastaken

Copy link
Copy Markdown
Member Author

Note to self: do we need FINAL here?

@skyfallwastaken

Copy link
Copy Markdown
Member Author

@greptileai review

@skyfallwastaken

Copy link
Copy Markdown
Member Author

@greptile-apps review

end
end
connection.execute(<<~SQL.squish)
INSERT INTO #{table} (#{column_list(connection)})
column == "user_id" ? older_user_id.to_i.to_s : connection.quote_column_name(column)
end
connection.execute(<<~SQL.squish)
INSERT INTO #{table} (#{column_list(connection)})
end
end
connection.execute(<<~SQL.squish)
INSERT INTO #{table} (#{column_list(connection)})
results << "user ##{newer_user.id} deleted"
end

Clickhouse::HeartbeatWriter.merge_user_heartbeats!(older_user_id: older_user.id, newer_user_id: newer_user.id)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Keep merge atomic
merge_user_heartbeats! now runs after the Postgres transaction has committed and after newer_user.destroy!. If the ClickHouse insert or tombstone fails, the newer user and related Postgres rows are already deleted while their heartbeats remain under the deleted user_id, so the account merge is only partially applied despite the rescue claiming rollback.

Artifacts

Repro: runtime version check output

  • Keeps the command output available without making the summary code-heavy.

Repro: Rails boot blocker from bundle check

  • Keeps the command output available without making the summary code-heavy.

Repro: executable account merge atomicity harness

  • Contains supporting evidence from the run (text/x-ruby; charset=utf-8).

Repro: harness output showing partial Postgres commit after ClickHouse failure

  • Keeps the command output available without making the summary code-heavy.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: app/controllers/admin/account_merger_controller.rb
Line: 109

Comment:
**Keep merge atomic**
`merge_user_heartbeats!` now runs after the Postgres transaction has committed and after `newer_user.destroy!`. If the ClickHouse insert or tombstone fails, the newer user and related Postgres rows are already deleted while their heartbeats remain under the deleted `user_id`, so the account merge is only partially applied despite the rescue claiming rollback.

How can I resolve this? If you propose a fix, please make it concise.

SELECT user_id,
time,
id,
arrayJoin([#{partition_array_sql}]) AS partition_value
attr_reader :user_id, :reason

def existing_deltas?
connection.select_value("SELECT 1 FROM #{delta_table} WHERE user_id = #{user_id} LIMIT 1").present?
def write_initial_facts
reason_sql = connection.quote(reason)
connection.execute(<<~SQL.squish)
INSERT INTO #{delta_table} (#{INSERT_COLUMNS.join(", ")})
nonzero = DELTA_COLUMNS.map { |column| "#{column} != 0" }.join(" OR ")

connection.execute(<<~SQL.squish)
INSERT INTO #{delta_table} (#{INSERT_COLUMNS.join(", ")})
Comment thread public/ch-pg-benchmark-report.html Fixed
Comment thread public/ch-pg-benchmark-report.html Fixed
Comment thread public/ch-pg-benchmark-report.html Fixed
@skyfallwastaken

Copy link
Copy Markdown
Member Author

@greptile-apps review

Comment thread .github/workflows/ci.yml
clickhouse:
image: clickhouse/clickhouse-server:26.6.1.1193
ports:
- 8123:8123

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Expose native port
The test job only publishes ClickHouse HTTP port 8123, but the new Clickhouse::ServingRepository uses clickhouse-native, whose native protocol connects to TCP port 9000 by default. Any test path that reaches StatsReader or serving-table reads will try localhost:9000 and fail in CI because that port is not mapped from the service container.

Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/ci.yml
Line: 155

Comment:
**Expose native port**
The test job only publishes ClickHouse HTTP port `8123`, but the new `Clickhouse::ServingRepository` uses `clickhouse-native`, whose native protocol connects to TCP port `9000` by default. Any test path that reaches `StatsReader` or serving-table reads will try `localhost:9000` and fail in CI because that port is not mapped from the service container.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread .github/workflows/ci.yml
clickhouse:
image: clickhouse/clickhouse-server:26.6.1.1193
ports:
- 8123:8123

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Expose native port
The system-test ClickHouse service has the same issue as the unit-test job: only 8123 is exposed, while Clickhouse::ServingRepository talks through the native client on port 9000 unless CLICKHOUSE_NATIVE_PORT overrides it. System tests that render project or profile stats through StatsReader will fail to connect to ClickHouse in CI.

Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/ci.yml
Line: 223

Comment:
**Expose native port**
The system-test ClickHouse service has the same issue as the unit-test job: only `8123` is exposed, while `Clickhouse::ServingRepository` talks through the native client on port `9000` unless `CLICKHOUSE_NATIVE_PORT` overrides it. System tests that render project or profile stats through `StatsReader` will fail to connect to ClickHouse in CI.

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants