Class: ActivePartition::PartitionManagers::TimeRange

Inherits:
Object
  • Object
show all
Defined in:
lib/active_partition/partition_managers/time_range.rb

Instance Method Summary collapse

Constructor Details

#initialize(partition_adapter, table_name, partition_start_from = nil) ⇒ TimeRange

Returns a new instance of TimeRange.



5
6
7
8
9
# File 'lib/active_partition/partition_managers/time_range.rb', line 5

def initialize(partition_adapter, table_name, partition_start_from = nil)
  @partition_adapter = partition_adapter
  @table_name = table_name
  @partition_start_from = partition_start_from
end

Instance Method Details

#active_partitions_cover?(value) ⇒ Boolean

Checks if the active partitions cover the given value.

Parameters:

  • value (Time)

    The value to check if it is covered by the active partitions.

Returns:

  • (Boolean)

    Returns true if the value is covered by any of the active partitions, otherwise returns false.



40
41
42
# File 'lib/active_partition/partition_managers/time_range.rb', line 40

def active_partitions_cover?(value)
  active_ranges.any? { |range| range.cover? value.utc }
end

#active_rangesArray

Retrieves the active ranges from the partition adapter.

The active ranges are cached in an instance variable ‘@active_ranges` to improve performance. If the `@active_ranges` variable is `nil`, the method calls the `reload_active_ranges` method with the result of `@partition_adapter.get_all_supported_partition_tables` as the argument.

Returns:

  • (Array)

    The array of active ranges.



18
19
20
21
22
23
# File 'lib/active_partition/partition_managers/time_range.rb', line 18

def active_ranges
  # in test environment, the partition table is not actually created. Therefore, return empty array
  return [] if defined?(Rails) && Rails.env.test?

  @active_ranges ||= reload_active_ranges(@partition_adapter.get_all_supported_partition_tables)
end

#build_partition_name(from, to) ⇒ String

Builds a partition name based on the given time range.

Parameters:

  • from (DateTime)

    The start time of the partition range.

  • to (DateTime)

    The end time of the partition range.

Returns:

  • (String)

    The generated partition name.



84
85
86
87
88
89
90
91
92
# File 'lib/active_partition/partition_managers/time_range.rb', line 84

def build_partition_name(from, to)
  unix_from = from.utc.to_i
  unix_to = to.utc.to_i

  # It's easier to manage when having readable part in the name
  readable_from = from.utc.strftime("%y%m%d_%H")

  "#{@table_name}_p_#{readable_from}_#{unix_from}_#{unix_to}"
end

#create_partition(from, to) ⇒ Range

Creates a new partition for the table based on the specified time range.

Parameters:

  • from (Time)

    The start time of the partition range.

  • to (Time)

    The end time of the partition range.

Returns:

  • (Range)

    The time range of the created partition.



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/active_partition/partition_managers/time_range.rb', line 99

def create_partition(from, to)
  from = from.utc
  to = to.utc

  partition_name = build_partition_name(from, to)
  puts "create partition #{partition_name} from #{from} to #{to}"
  @partition_adapter.exec_create_partition_by_time_range(partition_name, from, to)

  reload_active_ranges(@partition_adapter.get_all_supported_partition_tables)

  # rescue ActiveRecord::StatementInvalid => e
  # byebug
  # # When overlapping partition, the message will be like this:
  # # PG::InvalidObjectDefinition: ERROR:  partition "table_name_p_240626_09_1719395833_1719482233" would overlap partition "table_name_p_240627_09_1719481818_1719568218"
  # # LINE 3:   FOR VALUES FROM ('2024-06-26 09:57:13') TO ('2024-06-27 09
  # # catchup the floor of the from time to the conflict partition and retry
  # # handle the floor? what about the ceil?
  # if e.message.include?("would overlap partition")
  #   overlapped_partition = e.message.split("would overlap partition").last.split("\n").first.delete('"').strip
  #   overlapped_from, overlapped_to = overlapped_partition.split("_").last(2).map { |t| Time.at(t.to_i).utc }

  #   return true if (overlapped_from..overlapped_to).cover?(unix_from..unix_to)
  #   # unix_from < unix_to
  #   # overlapped_from < overlapped_to
  #   # if unix_from < overlapped_from
  #   #   overlapped_from = unix_from

  #   if floor_time > unix_from
  #     Rails.logger.warn "Retry create partition for #{unix_from} to #{floor_time}"
  #     create_partition(unix_from, floor_time)
  #   end
  # end
end

#latest_coverage_atTime

Returns the latest coverage time for the partition.

This method memoizes the latest coverage time by caching the result in an instance variable. If the latest coverage time has already been calculated, it will be returned from the cache. Otherwise, it will call the ‘latest_partition_coverage_time` method to calculate the latest coverage time.

Returns:

  • (Time)

    The latest coverage time for the partition.



51
52
53
# File 'lib/active_partition/partition_managers/time_range.rb', line 51

def latest_coverage_at
  @latest_coverage_at ||= latest_partition_coverage_time
end

#latest_partition_coverage_timeTime

Returns the coverage time of the latest partition.

If there are no supported partition tables, the coverage time will be the beginning of the current hour in UTC. Otherwise, the coverage time will be extracted from the latest partition table name.

Returns:

  • (Time)

    The coverage time of the latest partition in UTC.



139
140
141
142
143
144
145
146
147
# File 'lib/active_partition/partition_managers/time_range.rb', line 139

def latest_partition_coverage_time
  partition_tables = @partition_adapter.get_all_supported_partition_tables
  reload_active_ranges(partition_tables)
  return (@partition_start_from || Time.current.beginning_of_hour).utc if partition_tables.empty?

  latest_partition_table = partition_tables.sort_by { |p_name| p_name.split("_").last.to_i }.last
  @latest_coverage_at = Time.at(latest_partition_table.split("_").last.to_i).utc
  @latest_coverage_at
end

#premake(period = 1.month, number = 3, from = nil) ⇒ void

This method returns an undefined value.

Creates multiple partitions in the database based on the given period, number, and starting time.

Parameters:

  • period (ActiveSupport::Duration) (defaults to: 1.month)

    The duration of each partition.

  • number (Integer) (defaults to: 3)

    The number of partitions to create.

  • from (Time) (defaults to: nil)

    The starting time for creating partitions. If not provided, the current time is used.



156
157
158
159
160
161
162
163
164
# File 'lib/active_partition/partition_managers/time_range.rb', line 156

def premake(period = 1.month, number = 3, from = nil)
  new_latest_coverage_time = (from || Time.current).utc + (period * number)
  current_coverage_time = from || latest_partition_coverage_time

  while current_coverage_time < new_latest_coverage_time
    create_partition(current_coverage_time, current_coverage_time + period)
    current_coverage_time += period
  end
end

#prepare_partition(partitioned_value, period) ⇒ void

This method returns an undefined value.

Prepares a partition for the given partitioned value and period.

If the active partitions do not cover the partitioned value, a new partition is created.

Parameters:

  • partitioned_value (Time)

    The value to be partitioned.

  • period (Integer)

    The duration of each partition.



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/active_partition/partition_managers/time_range.rb', line 62

def prepare_partition(partitioned_value, period)
  return if active_partitions_cover?(partitioned_value)

  # when the latest_coverage_at is too far, the diff can be wrong (because leap years add up to the diff)
  # therefore, we need to calculate the diff based on the period
  # diff = (partitioned_value.utc - latest_coverage_at) / period
  # from_time = latest_coverage_at + (diff.floor * period)
  # to_time = from_time + period

  from_time = latest_coverage_at
  while !(from_time..(from_time + period)).cover?(partitioned_value) do
    from_time += period * (partitioned_value > from_time ? 1 : -1)
  end

  create_partition(from_time, from_time + period)
end

#reload_active_ranges(partition_names) ⇒ Array<Range>

Reloads the active ranges based on the given partition names.

Parameters:

  • partition_names (Array<String>)

    An array of partition names.

Returns:

  • (Array<Range>)

    An array of Range objects representing the active ranges.



29
30
31
32
33
34
# File 'lib/active_partition/partition_managers/time_range.rb', line 29

def reload_active_ranges(partition_names)
  @active_ranges = partition_names.map do |partition_name|
    start_at, end_at = partition_name.split("_").last(2).map { |t| Time.at(t.to_i).utc }
    (start_at...end_at)
  end
end

#remove_partitions(prunable_tables) ⇒ void

This method returns an undefined value.

Removes the specified partitions from the database.

Parameters:

  • prunable_tables (Array<String>)

    An array of partition names to be removed.



170
171
172
173
174
175
176
177
178
# File 'lib/active_partition/partition_managers/time_range.rb', line 170

def remove_partitions(prunable_tables)
  table_names = prunable_tables.each do |partition_name|
    @partition_adapter.detach_partition(partition_name)
    @partition_adapter.drop_partition(partition_name)
  end

  reload_active_ranges(@partition_adapter.get_all_supported_partition_tables)
  table_names
end

#retain(period = 1.months, number = 12, from = Time.current.utc) ⇒ void

This method returns an undefined value.

Retains a specified number of partition tables older than a given period.

Parameters:

  • period (ActiveSupport::Duration) (defaults to: 1.months)

    The duration of time to retain partitions.

  • number (Integer) (defaults to: 12)

    The number of partitions to retain.

  • from (Time) (defaults to: Time.current.utc)

    The reference time from which to calculate the retention period.



186
187
188
189
190
# File 'lib/active_partition/partition_managers/time_range.rb', line 186

def retain(period = 1.months, number = 12, from = Time.current.utc)
  prune_time = (from - (period * (number + 1))).utc

  retain_by_time(prune_time)
end

#retain_by_partition_count(retain_number) ⇒ Object



204
205
206
207
208
209
210
211
212
213
# File 'lib/active_partition/partition_managers/time_range.rb', line 204

def retain_by_partition_count(retain_number)
  partition_tables = @partition_adapter.get_all_supported_partition_tables
  nil if partition_tables.empty?

  current_partition_name = build_partition_name(Time.current, Time.current + 1.hour)
  past_partitions = partition_tables.select { |name| name <= current_partition_name }.sort
  prunable_partitions = past_partitions[.. -(retain_number + 2)] # -1 of current partition and -1 as syntax

  remove_partitions(prunable_partitions)
end

#retain_by_time(prune_time) ⇒ Object



192
193
194
195
196
197
198
199
200
201
202
# File 'lib/active_partition/partition_managers/time_range.rb', line 192

def retain_by_time(prune_time)
  partition_tables = @partition_adapter.get_all_supported_partition_tables
  return if partition_tables.empty?

  prunable_tables = partition_tables.select do |name|
    p_to_time = Time.at(name.split("_").last.to_i).utc
    p_to_time < prune_time
  end

  remove_partitions (prunable_tables)
end