partition_gardener 0.3.0
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +36 -0
- data/LICENSE.md +21 -0
- data/README.md +203 -0
- data/SECURITY.md +27 -0
- data/docs/THIRD_PARTY_LICENSE_MANIFEST.tsv +60 -0
- data/docs/application_contract.md +82 -0
- data/docs/audit_reference.md +125 -0
- data/docs/background_job.md +161 -0
- data/docs/cli.md +71 -0
- data/docs/configuration.md +261 -0
- data/docs/cutover.md +180 -0
- data/docs/decision_flow.md +154 -0
- data/docs/host_testing.md +91 -0
- data/docs/monitoring.md +110 -0
- data/docs/naming.md +71 -0
- data/docs/operations.md +131 -0
- data/docs/partition_landscape.md +323 -0
- data/docs/pg_party_recipe.md +44 -0
- data/docs/related_postgres_tooling.md +195 -0
- data/docs/retention.md +65 -0
- data/docs/schemas/partition_garden.schema.json +114 -0
- data/docs/schemas/plan_report.schema.json +82 -0
- data/docs/tooling_split.md +79 -0
- data/exe/partition_gardener +6 -0
- data/lib/partition_gardener/active_record_run_record_store.rb +1 -0
- data/lib/partition_gardener/advisory_lock.rb +48 -0
- data/lib/partition_gardener/archive_retention.rb +88 -0
- data/lib/partition_gardener/audit.rb +93 -0
- data/lib/partition_gardener/blank.rb +13 -0
- data/lib/partition_gardener/cli.rb +190 -0
- data/lib/partition_gardener/config_document.rb +240 -0
- data/lib/partition_gardener/configuration.rb +102 -0
- data/lib/partition_gardener/connection.rb +260 -0
- data/lib/partition_gardener/date_bucket.rb +117 -0
- data/lib/partition_gardener/date_calendar.rb +65 -0
- data/lib/partition_gardener/date_range_maintenance.rb +297 -0
- data/lib/partition_gardener/default_partition.rb +54 -0
- data/lib/partition_gardener/executor.rb +324 -0
- data/lib/partition_gardener/gap_detection.rb +59 -0
- data/lib/partition_gardener/hash_routing.rb +70 -0
- data/lib/partition_gardener/layout/calendar_year.rb +28 -0
- data/lib/partition_gardener/layout/integer_window.rb +28 -0
- data/lib/partition_gardener/layout/sliding_window.rb +28 -0
- data/lib/partition_gardener/layout/three_area.rb +27 -0
- data/lib/partition_gardener/layout/zone_segments.rb +62 -0
- data/lib/partition_gardener/lock_not_acquired.rb +3 -0
- data/lib/partition_gardener/maintenance_backend.rb +73 -0
- data/lib/partition_gardener/memory_run_record_store.rb +19 -0
- data/lib/partition_gardener/migration/hot_switch_concern.rb +445 -0
- data/lib/partition_gardener/missing_conflict_index.rb +3 -0
- data/lib/partition_gardener/naming.rb +29 -0
- data/lib/partition_gardener/orphaned_rebalance_staging.rb +3 -0
- data/lib/partition_gardener/pg_connection.rb +94 -0
- data/lib/partition_gardener/plan.rb +49 -0
- data/lib/partition_gardener/plan_applier.rb +289 -0
- data/lib/partition_gardener/plan_diff.rb +51 -0
- data/lib/partition_gardener/plan_report.rb +95 -0
- data/lib/partition_gardener/planner.rb +21 -0
- data/lib/partition_gardener/predicate.rb +85 -0
- data/lib/partition_gardener/premake_monthly_maintenance.rb +44 -0
- data/lib/partition_gardener/rails.rb +12 -0
- data/lib/partition_gardener/registry.rb +84 -0
- data/lib/partition_gardener/run_failed.rb +10 -0
- data/lib/partition_gardener/run_metrics.rb +65 -0
- data/lib/partition_gardener/run_record.rb +76 -0
- data/lib/partition_gardener/sql_run_record_store.rb +106 -0
- data/lib/partition_gardener/stdlib_extensions.rb +15 -0
- data/lib/partition_gardener/strategy/composite.rb +27 -0
- data/lib/partition_gardener/strategy/cursor_columns.rb +18 -0
- data/lib/partition_gardener/strategy/date_range.rb +303 -0
- data/lib/partition_gardener/strategy/hash_branches.rb +161 -0
- data/lib/partition_gardener/strategy/integer_range.rb +261 -0
- data/lib/partition_gardener/strategy/list_split.rb +125 -0
- data/lib/partition_gardener/strategy/requires_default_partition.rb +19 -0
- data/lib/partition_gardener/strategy.rb +26 -0
- data/lib/partition_gardener/templates.rb +373 -0
- data/lib/partition_gardener/unmoved_rows_remaining.rb +15 -0
- data/lib/partition_gardener/version.rb +3 -0
- data/lib/partition_gardener.rb +215 -0
- data/sig/partition_gardener.rbs +19 -0
- metadata +367 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/amkisko/partition_gardener.rb/main/docs/schemas/partition_garden.schema.json",
|
|
4
|
+
"title": "Partition Gardener table registration",
|
|
5
|
+
"description": "Portable fields for registering a partitioned parent table. Ruby lambdas (partition_name_format, partition_definition) are omitted; use layout templates instead.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["table_name", "layout", "partition_key_column", "conflict_key"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"table_name": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"minLength": 1
|
|
13
|
+
},
|
|
14
|
+
"layout": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"enum": [
|
|
17
|
+
"sliding_window",
|
|
18
|
+
"rolling_current",
|
|
19
|
+
"calendar_year",
|
|
20
|
+
"premake_monthly",
|
|
21
|
+
"integer_window",
|
|
22
|
+
"hash_branches"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"bucket": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"enum": ["day", "week", "month", "quarter", "year"]
|
|
28
|
+
},
|
|
29
|
+
"partition_key_column": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"minLength": 1
|
|
32
|
+
},
|
|
33
|
+
"conflict_key": {
|
|
34
|
+
"type": "array",
|
|
35
|
+
"items": { "type": "string" },
|
|
36
|
+
"minItems": 1
|
|
37
|
+
},
|
|
38
|
+
"active_days": {
|
|
39
|
+
"type": "integer",
|
|
40
|
+
"minimum": 1,
|
|
41
|
+
"maximum": 366
|
|
42
|
+
},
|
|
43
|
+
"active_weeks": {
|
|
44
|
+
"type": "integer",
|
|
45
|
+
"minimum": 1,
|
|
46
|
+
"maximum": 104
|
|
47
|
+
},
|
|
48
|
+
"active_months": {
|
|
49
|
+
"type": "integer",
|
|
50
|
+
"minimum": 1,
|
|
51
|
+
"maximum": 36
|
|
52
|
+
},
|
|
53
|
+
"active_quarters": {
|
|
54
|
+
"type": "integer",
|
|
55
|
+
"minimum": 1,
|
|
56
|
+
"maximum": 20
|
|
57
|
+
},
|
|
58
|
+
"active_years": {
|
|
59
|
+
"type": "integer",
|
|
60
|
+
"minimum": 1,
|
|
61
|
+
"maximum": 10
|
|
62
|
+
},
|
|
63
|
+
"premake_months": {
|
|
64
|
+
"type": "integer",
|
|
65
|
+
"minimum": 0,
|
|
66
|
+
"maximum": 24
|
|
67
|
+
},
|
|
68
|
+
"split_row_threshold": {
|
|
69
|
+
"type": "integer",
|
|
70
|
+
"minimum": 1
|
|
71
|
+
},
|
|
72
|
+
"move_batch_size": {
|
|
73
|
+
"type": "integer",
|
|
74
|
+
"minimum": 1
|
|
75
|
+
},
|
|
76
|
+
"statement_timeout": {
|
|
77
|
+
"type": "integer",
|
|
78
|
+
"minimum": 1,
|
|
79
|
+
"description": "Statement timeout in seconds for maintenance on this table"
|
|
80
|
+
},
|
|
81
|
+
"retention_months": {
|
|
82
|
+
"type": "integer",
|
|
83
|
+
"minimum": 1
|
|
84
|
+
},
|
|
85
|
+
"retention_apply": {
|
|
86
|
+
"type": "boolean",
|
|
87
|
+
"description": "When true, detach or drop archive partitions past retention_months; when false or omitted, log would-drop actions only"
|
|
88
|
+
},
|
|
89
|
+
"retention_keep_table": {
|
|
90
|
+
"type": "boolean"
|
|
91
|
+
},
|
|
92
|
+
"retention_detach_concurrently": {
|
|
93
|
+
"type": "boolean"
|
|
94
|
+
},
|
|
95
|
+
"hash_modulus": {
|
|
96
|
+
"type": "integer",
|
|
97
|
+
"minimum": 2,
|
|
98
|
+
"maximum": 128
|
|
99
|
+
},
|
|
100
|
+
"maintenance_backend": {
|
|
101
|
+
"type": "string",
|
|
102
|
+
"enum": ["gardener", "pg_partman", "hybrid_layout_only"]
|
|
103
|
+
},
|
|
104
|
+
"incremental_rebalance": {
|
|
105
|
+
"type": "boolean"
|
|
106
|
+
},
|
|
107
|
+
"run_record_enabled": {
|
|
108
|
+
"type": "boolean"
|
|
109
|
+
},
|
|
110
|
+
"analyze_after_rebalance": {
|
|
111
|
+
"type": "boolean"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/amkisko/partition_gardener.rb/main/docs/schemas/plan_report.schema.json",
|
|
4
|
+
"title": "Partition Gardener plan report",
|
|
5
|
+
"description": "Dry-run output from PartitionGardener.plan or run!(dry_run: true)",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": [
|
|
8
|
+
"schema_version",
|
|
9
|
+
"table_name",
|
|
10
|
+
"layout",
|
|
11
|
+
"changed",
|
|
12
|
+
"plan_signature",
|
|
13
|
+
"target_segments",
|
|
14
|
+
"attached_segments",
|
|
15
|
+
"operations",
|
|
16
|
+
"gaps",
|
|
17
|
+
"hot_buckets"
|
|
18
|
+
],
|
|
19
|
+
"additionalProperties": false,
|
|
20
|
+
"properties": {
|
|
21
|
+
"schema_version": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"const": "1.0"
|
|
24
|
+
},
|
|
25
|
+
"table_name": { "type": "string" },
|
|
26
|
+
"layout": { "type": "string" },
|
|
27
|
+
"changed": { "type": "boolean" },
|
|
28
|
+
"plan_signature": { "type": "string" },
|
|
29
|
+
"target_segments": {
|
|
30
|
+
"type": "array",
|
|
31
|
+
"items": { "$ref": "#/$defs/segment" }
|
|
32
|
+
},
|
|
33
|
+
"attached_segments": {
|
|
34
|
+
"type": "array",
|
|
35
|
+
"items": { "$ref": "#/$defs/segment" }
|
|
36
|
+
},
|
|
37
|
+
"operations": {
|
|
38
|
+
"type": "array",
|
|
39
|
+
"items": { "$ref": "#/$defs/operation" }
|
|
40
|
+
},
|
|
41
|
+
"gaps": {
|
|
42
|
+
"type": "array",
|
|
43
|
+
"items": { "$ref": "#/$defs/gap" }
|
|
44
|
+
},
|
|
45
|
+
"hot_buckets": {
|
|
46
|
+
"type": "array"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"$defs": {
|
|
50
|
+
"segment": {
|
|
51
|
+
"type": "object",
|
|
52
|
+
"required": ["name", "range_start", "range_end", "kind"],
|
|
53
|
+
"properties": {
|
|
54
|
+
"name": { "type": "string" },
|
|
55
|
+
"range_start": {},
|
|
56
|
+
"range_end": {},
|
|
57
|
+
"kind": { "type": "string" }
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"operation": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"required": ["action"],
|
|
63
|
+
"properties": {
|
|
64
|
+
"action": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"enum": ["keep", "create", "reshape", "drop"]
|
|
67
|
+
},
|
|
68
|
+
"segment": { "$ref": "#/$defs/segment" },
|
|
69
|
+
"attached_segment": { "$ref": "#/$defs/segment" }
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"gap": {
|
|
73
|
+
"type": "object",
|
|
74
|
+
"required": ["message"],
|
|
75
|
+
"properties": {
|
|
76
|
+
"range_start": {},
|
|
77
|
+
"range_end": {},
|
|
78
|
+
"message": { "type": "string" }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Tooling split: DDL creation, runtime maintenance, and extension maintenance
|
|
2
|
+
|
|
3
|
+
Partition work splits into three layers. Each layer has a different owner and schedule. Mixing owners on the same parent table without an explicit contract causes overlapping bounds, double drops, and rows stuck in the default partition.
|
|
4
|
+
|
|
5
|
+
## pg_party — creation (DDL)
|
|
6
|
+
|
|
7
|
+
Use during migrations and cutover.
|
|
8
|
+
|
|
9
|
+
- Create partitioned parent and first children
|
|
10
|
+
- Attach indexes across children
|
|
11
|
+
- Model helpers for partition DDL
|
|
12
|
+
|
|
13
|
+
pg_party does not replace nightly maintenance. After cutover, hand the table to runtime maintenance.
|
|
14
|
+
|
|
15
|
+
## pg_partman — extension runtime maintenance (premake and retention)
|
|
16
|
+
|
|
17
|
+
Use when the hosting environment allows the extension and the table is a plain time or id series.
|
|
18
|
+
|
|
19
|
+
- Premake upcoming intervals on a fixed cadence
|
|
20
|
+
- Retention via detach or drop
|
|
21
|
+
- Background worker or `run_maintenance_proc`
|
|
22
|
+
|
|
23
|
+
partman fits tables that do not need three-area layout, heat splits inside current, or default-last drain logic.
|
|
24
|
+
|
|
25
|
+
Register such tables with `maintenance_backend: :pg_partman` so Partition Gardener skips them.
|
|
26
|
+
|
|
27
|
+
## Partition Gardener — application runtime maintenance (layout)
|
|
28
|
+
|
|
29
|
+
Use for runtime maintenance when layout is more than fixed premake.
|
|
30
|
+
|
|
31
|
+
- Sliding window archive, current, and future
|
|
32
|
+
- Heat-driven splits inside current
|
|
33
|
+
- Mandatory default drain last
|
|
34
|
+
- Incremental tail rebalance and gap detection
|
|
35
|
+
- Hot-switch cutover helper
|
|
36
|
+
|
|
37
|
+
Gardener is the default `maintenance_backend: :gardener`.
|
|
38
|
+
|
|
39
|
+
## Pick one maintainer per table
|
|
40
|
+
|
|
41
|
+
New date-keyed OLTP table — create with pg_party or SQL; maintain with Gardener sliding window.
|
|
42
|
+
|
|
43
|
+
Plain monthly series when the extension is OK — create with pg_party or SQL; maintain with partman only (`maintenance_backend: :pg_partman`).
|
|
44
|
+
|
|
45
|
+
Migrating from premake-only cron — keep existing DDL; use Gardener after `premake_monthly` bridge, then upgrade template.
|
|
46
|
+
|
|
47
|
+
partman premake plus custom tail layout — create with pg_party or SQL; maintain with `maintenance_backend: :hybrid_layout_only` (gardener layout only; partman premake).
|
|
48
|
+
|
|
49
|
+
## Hybrid mode
|
|
50
|
+
|
|
51
|
+
`maintenance_backend: :hybrid_layout_only` means:
|
|
52
|
+
|
|
53
|
+
- pg_partman owns premake and simple retention on `partman.part_config`
|
|
54
|
+
- Gardener runs tail rebalance, default drain, and gap repair only
|
|
55
|
+
- Gardener must not create the same monthly bounds partman already premakes
|
|
56
|
+
|
|
57
|
+
On register, Gardener probes `partman.part_config` and warns when:
|
|
58
|
+
|
|
59
|
+
- `maintenance_backend: :gardener` but partman also lists the parent
|
|
60
|
+
- `maintenance_backend: :pg_partman` but partman has no row
|
|
61
|
+
- `hybrid_layout_only` but partman has no row
|
|
62
|
+
|
|
63
|
+
## Legacy premake template
|
|
64
|
+
|
|
65
|
+
`Templates.premake_monthly` is a stepping stone from cron premake jobs.
|
|
66
|
+
|
|
67
|
+
- Ensures current month through `premake_months` ahead exist
|
|
68
|
+
- Drains default
|
|
69
|
+
- Does not run sliding-window tail rebalance
|
|
70
|
+
|
|
71
|
+
Migrate to `sliding_window_monthly` when default stays empty and horizon warnings clear.
|
|
72
|
+
|
|
73
|
+
## Related
|
|
74
|
+
|
|
75
|
+
- [decision_flow.md](decision_flow.md) — when to partition and which layout
|
|
76
|
+
- [configuration.md](configuration.md) — `maintenance_backend` and per-table registry
|
|
77
|
+
- [background_job.md](background_job.md) — schedule gardener runs
|
|
78
|
+
- [cutover.md](cutover.md) — creation through hot switch
|
|
79
|
+
- [related_postgres_tooling.md](related_postgres_tooling.md) — ankane tools; pgslice hot-switch detail in [cutover.md](cutover.md#lessons-from-pgslice)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require_relative "sql_run_record_store"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module PartitionGardener
|
|
2
|
+
module AdvisoryLock
|
|
3
|
+
LOCK_NAMESPACE = "partition_gardener"
|
|
4
|
+
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def with_table_lock(table_name, &block)
|
|
8
|
+
case PartitionGardener.configuration.advisory_lock_mode
|
|
9
|
+
when :transaction
|
|
10
|
+
with_transaction_lock(table_name, &block)
|
|
11
|
+
else
|
|
12
|
+
with_session_lock(table_name, &block)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def with_session_lock(table_name)
|
|
17
|
+
connection = Connection.connection
|
|
18
|
+
lock_sql = lock_expression(connection, table_name)
|
|
19
|
+
acquired = false
|
|
20
|
+
acquired = connection.execute("SELECT pg_try_advisory_lock(#{lock_sql})").first.values.first
|
|
21
|
+
acquired = acquired == true || acquired == "t"
|
|
22
|
+
raise LockNotAcquired, "session advisory lock not acquired for #{table_name}" unless acquired
|
|
23
|
+
|
|
24
|
+
yield
|
|
25
|
+
ensure
|
|
26
|
+
connection.execute("SELECT pg_advisory_unlock(#{lock_sql})") if connection && lock_sql && acquired
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def with_transaction_lock(table_name)
|
|
30
|
+
connection = Connection.connection
|
|
31
|
+
lock_sql = lock_expression(connection, table_name)
|
|
32
|
+
|
|
33
|
+
connection.transaction do
|
|
34
|
+
acquired = connection.execute("SELECT pg_try_advisory_xact_lock(#{lock_sql})").first.values.first
|
|
35
|
+
acquired = acquired == true || acquired == "t"
|
|
36
|
+
raise LockNotAcquired, "transaction advisory lock not acquired for #{table_name}" unless acquired
|
|
37
|
+
|
|
38
|
+
yield
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def lock_expression(connection, table_name)
|
|
43
|
+
namespace = connection.quote(LOCK_NAMESPACE)
|
|
44
|
+
table = connection.quote(table_name)
|
|
45
|
+
"hashtext(#{namespace}), hashtext(#{table})"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
module PartitionGardener
|
|
2
|
+
class ArchiveRetention
|
|
3
|
+
include Naming
|
|
4
|
+
|
|
5
|
+
def initialize(config, executor: nil, job_class_name: "PartitionGardener")
|
|
6
|
+
@config = config
|
|
7
|
+
@executor = executor || Executor.for_config(config)
|
|
8
|
+
@job_class_name = job_class_name
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def apply!(dry_run: nil)
|
|
12
|
+
retention_months = @config[:retention_months]
|
|
13
|
+
return 0 unless retention_months
|
|
14
|
+
|
|
15
|
+
apply_retention = if dry_run.nil?
|
|
16
|
+
@config.fetch(:retention_apply, false)
|
|
17
|
+
else
|
|
18
|
+
!dry_run
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
strategy = Strategy.for(@config)
|
|
22
|
+
return 0 unless strategy.is_a?(Strategy::DateRange)
|
|
23
|
+
|
|
24
|
+
table_name = @config[:table_name]
|
|
25
|
+
cutoff = DateCalendar.add_months(PartitionGardener.configuration.today, -retention_months)
|
|
26
|
+
dropped = 0
|
|
27
|
+
managed_tail_names = managed_tail_name_set(strategy)
|
|
28
|
+
|
|
29
|
+
Connection.attached_partitions(table_name).each do |partition|
|
|
30
|
+
next if partition.default
|
|
31
|
+
next if skip_retention_partition?(strategy, partition.name, managed_tail_names)
|
|
32
|
+
|
|
33
|
+
bucket = strategy.archive_bucket_from_partition_name(partition.name)
|
|
34
|
+
next unless bucket
|
|
35
|
+
next if bucket >= DateCalendar.beginning_of_month(cutoff)
|
|
36
|
+
|
|
37
|
+
if apply_retention
|
|
38
|
+
@executor.detach_partition(table_name, partition.name, concurrently: detach_concurrently?)
|
|
39
|
+
@executor.drop_table(partition.name) unless @config.fetch(:retention_keep_table, false)
|
|
40
|
+
|
|
41
|
+
PartitionGardener.configuration.notify(
|
|
42
|
+
"[PartitionGardener] Dropped archive partition #{partition.name} (retention #{retention_months} months)",
|
|
43
|
+
context: {
|
|
44
|
+
table_name: table_name,
|
|
45
|
+
partition_name: partition.name,
|
|
46
|
+
retention_months: retention_months,
|
|
47
|
+
job: @job_class_name
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
dropped += 1
|
|
51
|
+
next
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
PartitionGardener.configuration.notify(
|
|
55
|
+
"[PartitionGardener] Would drop archive partition #{partition.name} (bucket #{bucket}, retention #{retention_months} months)",
|
|
56
|
+
context: {
|
|
57
|
+
table_name: table_name,
|
|
58
|
+
partition_name: partition.name,
|
|
59
|
+
bucket: bucket,
|
|
60
|
+
dry_run: true
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
dropped += 1
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
dropped
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def detach_concurrently?
|
|
72
|
+
@config.fetch(:retention_detach_concurrently, PartitionGardener.configuration.retention_detach_concurrently)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def managed_tail_name_set(strategy)
|
|
76
|
+
Set.new(strategy.managed_tail_partition_names)
|
|
77
|
+
rescue NoMethodError
|
|
78
|
+
Set.new
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def skip_retention_partition?(strategy, partition_name, managed_tail_names)
|
|
82
|
+
(strategy.respond_to?(:tail_slot_name?) && strategy.tail_slot_name?(partition_name)) ||
|
|
83
|
+
managed_tail_names.include?(partition_name)
|
|
84
|
+
rescue NoMethodError
|
|
85
|
+
false
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
module PartitionGardener
|
|
2
|
+
class Audit
|
|
3
|
+
SCHEMA_VERSION = "1.0"
|
|
4
|
+
HORIZON_WARNING_DAYS = 30
|
|
5
|
+
|
|
6
|
+
AuditResult = Data.define(
|
|
7
|
+
:table_name,
|
|
8
|
+
:partitioned,
|
|
9
|
+
:default_row_count,
|
|
10
|
+
:attached_child_count,
|
|
11
|
+
:horizon_days,
|
|
12
|
+
:gaps,
|
|
13
|
+
:warnings
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
def self.call(table_name, config: Registry.find_by_table_name(table_name))
|
|
17
|
+
new(table_name, config: config).call
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(table_name, config: nil)
|
|
21
|
+
@table_name = table_name
|
|
22
|
+
@config = config || Registry.find_by_table_name(table_name)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def call
|
|
26
|
+
unless Connection.table_is_partitioned?(@table_name)
|
|
27
|
+
return AuditResult.new(
|
|
28
|
+
table_name: @table_name,
|
|
29
|
+
partitioned: false,
|
|
30
|
+
default_row_count: 0,
|
|
31
|
+
attached_child_count: 0,
|
|
32
|
+
horizon_days: nil,
|
|
33
|
+
gaps: [],
|
|
34
|
+
warnings: ["#{@table_name} is not a partitioned table"]
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
default_name = Naming.default_partition_name(@table_name)
|
|
39
|
+
default_row_count = if Connection.partition_exists?(default_name)
|
|
40
|
+
Connection.count_rows_in_partition_table(default_name)
|
|
41
|
+
else
|
|
42
|
+
0
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
partitions = Connection.attached_partitions(@table_name)
|
|
46
|
+
warnings = []
|
|
47
|
+
warnings << "default partition #{default_name} has #{default_row_count} rows" if default_row_count.positive?
|
|
48
|
+
warnings << "default partition #{default_name} is missing" unless Connection.partition_exists?(default_name)
|
|
49
|
+
|
|
50
|
+
horizon_days = compute_horizon_days(partitions)
|
|
51
|
+
if horizon_days && horizon_days < HORIZON_WARNING_DAYS
|
|
52
|
+
warnings << "partition horizon is #{horizon_days} days ahead (below #{HORIZON_WARNING_DAYS})"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
if partitions.size > 200
|
|
56
|
+
warnings << "attached child count is #{partitions.size} (high catalog pressure)"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
gaps = GapDetection.call(@table_name, config: @config)
|
|
60
|
+
gaps.each do |gap|
|
|
61
|
+
warnings << "partition gap: #{gap.message}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
AuditResult.new(
|
|
65
|
+
table_name: @table_name,
|
|
66
|
+
partitioned: true,
|
|
67
|
+
default_row_count: default_row_count,
|
|
68
|
+
attached_child_count: partitions.size,
|
|
69
|
+
horizon_days: horizon_days,
|
|
70
|
+
gaps: gaps,
|
|
71
|
+
warnings: warnings
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def compute_horizon_days(partitions)
|
|
78
|
+
today = PartitionGardener.configuration.today
|
|
79
|
+
finite_ends = partitions.filter_map do |partition|
|
|
80
|
+
next if partition.default
|
|
81
|
+
next if partition.range_end == :max
|
|
82
|
+
next unless partition.range_end.is_a?(Date)
|
|
83
|
+
|
|
84
|
+
partition.range_end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
return nil if finite_ends.empty?
|
|
88
|
+
|
|
89
|
+
max_end = finite_ends.max
|
|
90
|
+
(max_end - today).to_i
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|