Freika/dawarich · error · Stats::HexagonCalculator::PostGISError
Failed to calculate H3 hexagon centers: #{e.message}
Error message
Failed to calculate H3 hexagon centers: #{e.message} What it means
Stats::HexagonCalculator::PostGISError raised from the rescue in calculate_hexagons: every StandardError thrown while batching through the user's monthly points (each_coordinate_batch) and computing H3 indexes (build_h3_hash) is wrapped and re-raised as PostGISError with the original message appended. Despite the class name, PostGIS itself is only one possible cause - H3 conversion failures, Date argument errors from start_timestamp/end_timestamp (DateTime.new with an invalid year/month), or a dropped PG connection all surface identically here. ExceptionReporter is called first when defined.
Source
Thrown at app/services/stats/hexagon_calculator.rb:62
if h3_hash.empty?
Rails.logger.info "No H3 hex IDs calculated for user #{user.id}, #{year}-#{month} (no data)"
return nil
end
if h3_hash.size > MAX_HEXAGONS
Rails.logger.warn "Too many hexagons (#{h3_hash.size}), using lower resolution"
# Try with lower resolution (larger hexagons)
lower_resolution = [h3_resolution - 2, 0].max
Rails.logger.info "Recalculating with lower H3 resolution: #{lower_resolution}"
return calculate_hexagons(lower_resolution)
end
Rails.logger.info "Generated #{h3_hash.size} H3 hexagons at resolution #{h3_resolution} for user #{user.id}"
h3_hash
rescue StandardError => e
message = "Failed to calculate H3 hexagon centers: #{e.message}"
ExceptionReporter.call(e, message) if defined?(ExceptionReporter)
raise PostGISError, message
end
def start_timestamp
(DateTime.new(year, month, 1) - 2.days).to_i
end
def end_timestamp
(DateTime.new(year, month, -1, 23, 59, 59) + 2.days).to_i
end
def points
return @points if defined?(@points)
tz = user.timezone_iana
@points = user
.points
.not_anomaly
.without_raw_dataView on GitHub (pinned to 97fad417c5)
Solutions
- Read the message suffix - it is the wrapped original error ('Failed to calculate H3 hexagon centers: <original>') and points at the real cause
- For H3/coordinate errors, run Points::AnomalyFilter over the affected range, then retry the job
- For timeouts, raise the statement timeout or split the range; the calculator already batches at 50k rows and auto-lowers H3 resolution above 10k hexagons
- Validate year/month are real calendar values (1..12, plausible year) before enqueuing Stats::CalculatingJob
Example fix
# before: job body lets PostGISError kill the job with no trace of the cause
Stats::HexagonCalculator.new(user_id, year, month).call
# after: rescue, log full cause, and re-enqueue once
begin
Stats::HexagonCalculator.new(user_id, year, month).call
rescue Stats::HexagonCalculator::PostGISError => e
Rails.logger.error("hexagons failed for #{year}-#{month}: #{e.message} cause=#{e.cause&.message}")
retry_job wait: 5.minutes if executions < 2
end Defensive patterns
Strategy: try-catch
Validate before calling
# Reject impossible periods before enqueuing stats jobs raise ArgumentError, 'bad period' unless year.between?(1970, 2100) && month.between?(1, 12)
Type guard
def calculable_period?(year, month) year.to_i.between?(1970, 2100) && month.to_i.between?(1, 12) end
Try / catch
begin
Stats::HexagonCalculator.new(user_id, year, month).call
rescue Stats::HexagonCalculator::PostGISError => e
# e.message ends with the wrapped original - log e.cause for the real stack
Rails.logger.error("hexagons failed #{year}-#{month}: #{e.message} cause=#{e.cause&.class}")
retry_job wait: 10.minutes if executions < 3 # transient DB faults benefit from retry
end Prevention
- Always log e.cause - PostGISError is a wrapper, the real class is underneath
- Run Points::AnomalyFilter before regenerating stats for ranges with suspect GPS data
- Cap statement timeouts vs. batch size when users have very dense months
When it happens
Trigger: Stats::CalculatingJob running for a user/month whose points include coordinates H3 rejects (NaN, infinity, out-of-range lat/lng) via H3.from_geo_coordinates; an invalid year/month (0, 13, negative) making DateTime.new raise; a query timeout or lost connection mid-batch on a very large month.
Common situations: Bad GPS data imported before anomaly filtering runs, stats jobs enqueued with garbage year/month values, statement timeouts on months with hundreds of thousands of points, or transient DB failovers during background stat recomputation.
AI-assisted analysis of Freika/dawarich@97fad417c5 (2026-08-21).
Data as JSON: /api/errors/a3655957f55b6039.
Report an issue: GitHub.