Skip to content

Instantly share code, notes, and snippets.

@jonatas
Created September 14, 2021 16:07
Show Gist options
  • Save jonatas/ea66f9e58a60145388177fc881a740b2 to your computer and use it in GitHub Desktop.
Save jonatas/ea66f9e58a60145388177fc881a740b2 to your computer and use it in GitHub Desktop.
require_relative 'migration_helpers'
require_relative 'hypertable_helpers'
module Timescale
class Chunk < ActiveRecord::Base
self.table_name = "timescaledb_information.chunks"
def compress!
execute("SELECT compress_chunk(#{chunk_relation})")
end
def decompress!
execute("SELECT decompress_chunk(#{chunk_relation})")
end
def chunk_relation
"('#{chunk_schema}.#{chunk_name}')::regclass"
end
def execute(sql)
self.class.connection.execute(sql)
end
end
end
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby '2.6.5'
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 6.0.3', '>= 6.0.3.6'
# Use postgresql as the database for Active Record
gem 'pg', '>= 0.18', '< 2.0'
# Use Puma as the app server
gem 'puma', '~> 4.1'
# Use SCSS for stylesheets
gem 'sass-rails', '>= 6'
# Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
gem 'turbolinks', '~> 5'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.7'
# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 4.0'
# Use Active Model has_secure_password
# gem 'bcrypt', '~> 3.1.7'
# Use Active Storage variant
# gem 'image_processing', '~> 1.2'
# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', '>= 1.4.2', require: false
group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
end
group :development do
# Access an interactive console on exception pages or by calling 'console' anywhere in the code.
gem 'web-console', '>= 3.3.0'
gem 'listen', '~> 3.2'
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
gem 'spring'
gem 'spring-watcher-listen', '~> 2.0.0'
end
group :test do
# Adds support for Capybara system testing and selenium driver
gem 'capybara', '>= 2.15'
gem 'selenium-webdriver'
# Easy installation and use of web drivers to run system tests with browsers
gem 'webdrivers'
end
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
gem "pry-rails", "~> 0.3.9"
gem "dotenv-rails"
gem "rspec-rails", "~> 5.0"
module Timescale
class Hypertable < ActiveRecord::Base
self.table_name = "timescaledb_information.hypertables"
end
end
require_relative 'chunk'
require_relative 'hypertable'
module Timescale
module HypertableHelpers
extend ActiveSupport::Concern
included do
scope :chunks, -> () do
Chunk.where(hypertable_name: self.table_name)
end
scope :hypertable, -> () do
Hypertable.where(hypertable_name: self.table_name)
end
scope :last_month, -> { where('created_at > ?', 1.month.ago) }
scope :last_week, -> { where('created_at > ?', 1.week.ago) }
scope :last_hour, -> { where('created_at > ?', 1.hour.ago) }
scope :yesterday, -> { where('DATE(created_at) = ?', 1.day.ago.to_date) }
scope :today, -> { where('DATE(created_at) = ?', Date.today) }
scope :counts_per, -> (time_dimension) {
select("time_bucket('#{time_dimension}', created_at) as time, identifier, count(1) as total")
.group(:time, :identifier).order(:time)
.map {|result| [result.time, result.identifier, result.total]}
}
scope :detailed_size, -> do
self.connection.execute("SELECT * from chunks_detailed_size('#{self.table_name}')")
.map(&OpenStruct.method(:new))
end
scope :compression_stats, -> do
self.connection.execute("SELECT * from hypertable_compression_stats('#{self.table_name}')")
.map(&OpenStruct.method(:new))
end
end
end
end
require 'active_record/connection_adapters/postgresql_adapter'
# Useful methods to run TimescaleDB in you Ruby app.
module Timescale
# Migration helpers can help you to setup hypertables by default.
module MigrationHelpers
# create_table can receive `hypertable` argument
# @example
# options = {
# time_column: 'created_at',
# chunk_time_interval: '1 min',
# compress_segmentby: 'identifier',
# compression_interval: '7 days'
# }
#
# create_table(:events, id: false, hypertable: options) do |t|
# t.string :identifier, null: false
# t.jsonb :payload
# t.timestamps
# end
def create_table(table_name, id: :primary_key, primary_key: nil, force: nil, **options)
super
setup_hypertable_options(table_name, **options[:hypertable]) if options.key?(:hypertable)
end
# Setup hypertable from options
# @see create_table with the hypertable options.
def setup_hypertable_options(table_name,
time_column: 'created_at',
chunk_time_interval: '1 week',
compress_segmentby: nil,
compression_interval: nil
)
execute "SELECT create_hypertable('#{table_name}', '#{time_column}',
chunk_time_interval => INTERVAL '#{chunk_time_interval}')"
if compress_segmentby
execute <<~SQL
ALTER TABLE events SET (
timescaledb.compress,
timescaledb.compress_segmentby = '#{compress_segmentby}'
)
SQL
end
if compression_interval
execute "SELECT add_compression_policy('#{table_name}', INTERVAL '#{compression_interval}')"
end
end
end
end
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.include(Timescale::MigrationHelpers)
class PageLoad < ApplicationRecord
include Timescale::HypertableHelpers
attribute :value
scope :per_minute, -> { time_bucket('1 minute') }
scope :per_hour, -> { time_bucket('1 hour') }
scope :per_day, -> { time_bucket('1 day') }
scope :per_week, -> { time_bucket('1 week') }
scope :per_month, -> { time_bucket('1 month') }
scope :average_response_time_per_minute, -> { time_bucket('1 minute', value: 'avg(performance)') }
scope :average_response_time_per_hour, -> { time_bucket('1 hour', value: 'avg(performance)') }
scope :worst_response_time_last_minute, -> { time_bucket('1 minute', value: 'max(performance)') }
scope :worst_response_time_last_hour, -> { time_bucket('1 hour', value: 'max(performance)') }
scope :best_response_time_last_hour, -> { time_bucket('1 hour', value: 'min(performance)') }
scope :paths, -> { distinct.pluck(:path) }
def self.statistics
paths.each_with_object({}) do |path,resume |
resume[path] = resume_for(path)
end
end
def self.resume_for(path)
filter = where(path: path)
get = -> (scope_name) { filter.send(scope_name).first.value}
metrics.each_with_object({}) do |metric, resume|
resume[metric] = get[metric]
end
end
def self.metrics
methods.grep /response_time/
end
end
require 'rails_helper'
RSpec.describe PageLoad do
describe "#create" do
it "works as a hypertable" do
expect do
PageLoad.create user_agent: "test", path: "/", performance: 0.4
end.to change { PageLoad.chunks.size }.from(0).to(1)
end
end
end
# This file is copied to spec/ when you run 'rails generate rspec:install'
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../config/environment', __dir__)
# Prevent database truncation if the environment is production
abort("The Rails environment is running in production mode!") if Rails.env.production?
require 'rspec/rails'
# Add additional requires below this line. Rails is not loaded until this point!
# Requires supporting ruby files with custom matchers and macros, etc, in
# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
# run as spec files by default. This means that files in spec/support that end
# in _spec.rb will both be required and run as specs, causing the specs to be
# run twice. It is recommended that you do not name files matching this glob to
# end with _spec.rb. You can configure this pattern with the --pattern
# option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
#
# The following line is provided for convenience purposes. It has the downside
# of increasing the boot-up time by auto-requiring all files in the support
# directory. Alternatively, in the individual `*_spec.rb` files, manually
# require only the support files necessary.
#
# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }
# Checks for pending migrations and applies them before tests are run.
# If you are not using ActiveRecord, you can remove these lines.
begin
ActiveRecord::Migration.maintain_test_schema!
rescue ActiveRecord::PendingMigrationError => e
puts e.to_s.strip
exit 1
end
RSpec.configure do |config|
# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
config.fixture_path = "#{::Rails.root}/spec/fixtures"
# If you're not using ActiveRecord, or you'd prefer not to run each of your
# examples within a transaction, remove the following line or assign false
# instead of true.
config.use_transactional_fixtures = true
# You can uncomment this line to turn off ActiveRecord support entirely.
# config.use_active_record = false
# RSpec Rails can automatically mix in different behaviours to your tests
# based on their file location, for example enabling you to call `get` and
# `post` in specs under `spec/controllers`.
#
# You can disable this behaviour by removing the line below, and instead
# explicitly tag your specs with their type, e.g.:
#
# RSpec.describe UsersController, type: :controller do
# # ...
# end
#
# The different available types are documented in the features, such as in
# https://relishapp.com/rspec/rspec-rails/docs
config.infer_spec_type_from_file_location!
# Filter lines from Rails gems in backtraces.
config.filter_rails_from_backtrace!
# arbitrary gems may also be filtered via:
# config.filter_gems_from_backtrace("gem name")
#
# Timescale Befor Start Suite, set Hypertables
config.before(:suite) do
hypertable_models = ApplicationRecord
.descendants
.select{|clazz| clazz.ancestors.include?( Timescale::HypertableHelpers)}
hypertable_models.each do |clazz|
if clazz.hypertable.exists?
ApplicationRecord.logger.info "skip recreating hypertable for '#{clazz.table_name}'."
next
end
ApplicationRecord.connection.execute <<~SQL
SELECT create_hypertable('#{clazz.table_name}', 'created_at')
SQL
end
end
end
require 'timescale/all'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment