Skip to content

Implement autovacuum hooks #9

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions contrib/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ SUBDIRS = \
btree_gist \
citext \
cube \
custom_autovacuum \
dblink \
dict_int \
dict_xsyn \
Expand Down
23 changes: 23 additions & 0 deletions contrib/custom_autovacuum/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# contrib/custom_autovacuum/Makefile

MODULE_big = custom_autovacuum
OBJS = \
$(WIN32RES) \
custom_autovacuum.o
PGFILEDESC = "custom_autovacuum - Custom autovacuum policy framework"

EXTENSION = custom_autovacuum
DATA = custom_autovacuum--1.0.sql

REGRESS =

ifdef USE_PGXS
PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)
else
subdir = contrib/custom_autovacuum
top_builddir = ../..
include $(top_builddir)/src/Makefile.global
include $(top_srcdir)/contrib/contrib-global.mk
endif
122 changes: 122 additions & 0 deletions contrib/custom_autovacuum/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Custom Autovacuum Extension

A PostgreSQL extension that provides a custom autovacuum policy framework, allowing you to override the default autovacuum decision logic with a single adaptive policy.

## Overview

This extension implements a proportional controller-based autovacuum policy that dynamically adjusts vacuum aggressiveness based on the rate of dead tuple accumulation. The policy:

- **Adapts to workload patterns**: More aggressive vacuuming when dead tuples accumulate quickly
- **Minimizes overhead**: Conservative vacuuming when dead tuples accumulate slowly
- **Uses proportional control**: Dynamically adjusts thresholds based on accumulation rate
- **Provides cost control**: Adjusts vacuum cost limits and delays based on workload

## Installation

### 1. Build the Extension

```bash
cd contrib/custom_autovacuum
make
make install
```

### 2. Configure PostgreSQL

Add to `postgresql.conf`:

```ini
shared_preload_libraries = 'custom_autovacuum'
```

### 3. Create the Extension

```sql
CREATE EXTENSION custom_autovacuum;
```

## How It Works

The extension hooks into PostgreSQL's autovacuum decision process and replaces the default threshold-based logic with an adaptive policy:

### Proportional Controller Logic

1. **Calculate accumulation rate**: Dead tuples per second since last vacuum
2. **Compare to target rate**: Default target is 0.1 dead tuples/second
3. **Adjust threshold**: Lower threshold for high accumulation, higher for low accumulation
4. **Set cost parameters**: Aggressive settings for high rates, conservative for low rates

### Policy Behavior

- **High accumulation rate** (>0.2 dead tuples/sec): Aggressive vacuum (cost_limit=2000, delay=0)
- **Moderate accumulation rate** (0.1-0.2 dead tuples/sec): Balanced vacuum (cost_limit=1000, delay=5ms)
- **Low accumulation rate** (<0.1 dead tuples/sec): Conservative vacuum (cost_limit=500, delay=10ms)

## Configuration

### GUC Variables

- `custom_autovacuum.enabled` (boolean): Enable/disable the custom policy (default: true)

### Example Configuration

```sql
-- Enable custom autovacuum policy
SET custom_autovacuum.enabled = true;

-- Check current setting
SHOW custom_autovacuum.enabled;
```

## Monitoring

Custom policy decisions are logged at DEBUG2 level. Enable debug logging to see policy decisions:

```sql
SET log_min_messages = 'debug2';
```

For detailed proportional controller decisions, use DEBUG3:

```sql
SET log_min_messages = 'debug3';
```

## Example Log Output

```
DEBUG: Custom autovacuum policy: high accumulation rate (0.25 dead tuples/sec), aggressive vacuum
DEBUG3: Custom autovacuum policy: dead_tuples=1500, time_since_vacuum=6000000 ms, rate=0.25/sec, base_thresh=1000, adj_thresh=750, error=0.15
```

## Limitations

- Only one policy can be active (the proportional controller)
- Policies cannot override wraparound vacuum requirements

## Troubleshooting

1. **Extension not loading**: Check `shared_preload_libraries` in postgresql.conf
2. **Policy not working**: Verify the extension is created in the database
3. **No debug output**: Set `log_min_messages = 'debug2'` and restart

## Customization

To modify the policy behavior, edit the `custom_autovacuum_policy()` function in `custom_autovacuum.c`:

```c
/* Proportional controller parameters */
kp = 0.1; /* Proportional gain - adjust this to control sensitivity */
target_rate = 0.1; /* Target dead tuple rate (dead tuples per second) */
min_threshold = 50.0; /* Minimum threshold */
max_threshold = base_threshold * 3.0; /* Maximum threshold */
```

## Contributing

To modify the policy:

1. Edit the `custom_autovacuum_policy()` function in `custom_autovacuum.c`
2. Rebuild and reinstall the extension
3. Restart PostgreSQL
4. Test thoroughly with various table sizes and update patterns
6 changes: 6 additions & 0 deletions contrib/custom_autovacuum/custom_autovacuum--1.0.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- Custom autovacuum policy framework
-- This extension provides a framework for implementing custom autovacuum
-- policies as C functions.

-- Create a schema for organization
CREATE SCHEMA IF NOT EXISTS custom_autovacuum;
199 changes: 199 additions & 0 deletions contrib/custom_autovacuum/custom_autovacuum.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/*-------------------------------------------------------------------------
*
* custom_autovacuum.c
* Custom autovacuum policy framework
*
* This extension provides a framework for implementing custom autovacuum
* policies as C functions. It allows users to override the default
* autovacuum decision logic with a single custom policy.
*
* Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
*
* IDENTIFICATION
* contrib/custom_autovacuum/custom_autovacuum.c
*
*-------------------------------------------------------------------------
*/
#include "postgres.h"

#include <limits.h>
#include <string.h>

#include "fmgr.h"
#include "utils/guc.h"
#include "utils/memutils.h"
#include "utils/timestamp.h"
#include "pgstat.h"
#include "access/htup_details.h"
#include "utils/rel.h"
#include "postmaster/autovacuum.h"
#include "custom_autovacuum.h"

PG_MODULE_MAGIC_EXT(
.name = "custom_autovacuum",
.version = PG_VERSION
);

/* Global variables */
static custom_autovacuum_policy_hook_type original_hook = NULL;
static bool custom_autovacuum_enabled = true;

/* Hook variable - declared in autovacuum.c, accessible to extensions */
extern PGDLLIMPORT custom_autovacuum_policy_hook_type custom_autovacuum_policy_hook;

/*
* Single custom autovacuum policy function
*
* This policy uses a proportional controller to dynamically adjust vacuum
* aggressiveness based on the rate of dead tuple accumulation. The idea is:
* - If dead tuples are accumulating quickly, vacuum more aggressively (lower threshold)
* - If dead tuples are accumulating slowly, vacuum less aggressively (higher threshold)
*
* The controller calculates the rate of dead tuple accumulation and adjusts
* the vacuum threshold proportionally.
*/
static CustomAutovacuumPolicyResult
custom_autovacuum_policy(CustomAutovacuumPolicyContext *context)
{
CustomAutovacuumPolicyResult result = {0};
float4 current_dead_tuples;
TimestampTz last_vacuum;
TimestampTz now;
double time_since_last_vacuum_ms;
double dead_tuple_rate_per_ms;
double dead_tuple_rate_per_sec;
float4 base_threshold;
float4 kp;
float4 target_rate;
float4 min_threshold;
float4 max_threshold;
float4 error;
float4 adjusted_threshold;

if (!context->stats)
return result;

/* Get current statistics */
current_dead_tuples = context->stats->dead_tuples;
last_vacuum = context->stats->last_vacuum_time;
now = GetCurrentTimestamp();

/* Calculate time since last vacuum in milliseconds (matching PostgreSQL's approach) */
time_since_last_vacuum_ms = 0;
if (last_vacuum > 0)
{
time_since_last_vacuum_ms = (now - last_vacuum) / 1000.0; /* Convert to milliseconds */
}

/* If no vacuum has been done yet or very recent, use a conservative approach */
if (time_since_last_vacuum_ms <= 0 || time_since_last_vacuum_ms < 60000) /* Less than 1 minute */
{
/* Use default threshold for new tables */
if (current_dead_tuples > context->vacthresh)
{
result.should_vacuum = true;
result.reason = "Custom policy: new table with high dead tuple count";
}
return result;
}

/* Calculate dead tuple accumulation rate (dead tuples per millisecond, then convert to per second)
* This matches the approach in vacuum_log_rates() where rates are calculated as:
* rate = count / time_in_milliseconds
*/
dead_tuple_rate_per_ms = current_dead_tuples / time_since_last_vacuum_ms;
dead_tuple_rate_per_sec = dead_tuple_rate_per_ms * 1000.0; /* Convert to per second */

/* Base threshold from context (this is the default autovacuum threshold) */
base_threshold = context->vacthresh;

/* Proportional controller parameters */
kp = 0.1; /* Proportional gain - adjust this to control sensitivity */
target_rate = 0.1; /* Target dead tuple rate (dead tuples per second) */
min_threshold = 50.0; /* Minimum threshold */
max_threshold = base_threshold * 3.0; /* Maximum threshold */

/* Calculate error (difference between current rate and target rate) */
error = dead_tuple_rate_per_sec - target_rate;

/* Apply proportional control to adjust threshold */
adjusted_threshold = base_threshold - (kp * error * base_threshold);

/* Clamp threshold to reasonable bounds */
if (adjusted_threshold < min_threshold)
adjusted_threshold = min_threshold;
if (adjusted_threshold > max_threshold)
adjusted_threshold = max_threshold;

/* Determine if we should vacuum based on adjusted threshold */
if (current_dead_tuples > adjusted_threshold)
{
result.should_vacuum = true;

/* Adjust cost parameters based on accumulation rate */
if (dead_tuple_rate_per_sec > target_rate * 2.0) /* High accumulation rate */
{
result.custom_vac_cost_limit = 2000; /* Higher cost limit for aggressive vacuum */
result.custom_vac_cost_delay = 0.0; /* No delay for aggressive vacuum */
result.reason = pstrdup(psprintf("Custom policy: high accumulation rate (%.2f dead tuples/sec), aggressive vacuum", dead_tuple_rate_per_sec));
}
else if (dead_tuple_rate_per_sec > target_rate) /* Moderate accumulation rate */
{
result.custom_vac_cost_limit = 1000; /* Moderate cost limit */
result.custom_vac_cost_delay = 5.0; /* Small delay */
result.reason = pstrdup(psprintf("Custom policy: moderate accumulation rate (%.2f dead tuples/sec)", dead_tuple_rate_per_sec));
}
else /* Low accumulation rate */
{
result.custom_vac_cost_limit = 500; /* Lower cost limit */
result.custom_vac_cost_delay = 10.0; /* Higher delay */
result.reason = pstrdup(psprintf("Custom policy: low accumulation rate (%.2f dead tuples/sec), conservative vacuum", dead_tuple_rate_per_sec));
}

/* Log the controller decision for debugging (matching PostgreSQL's logging format) */
elog(DEBUG3, "Custom autovacuum policy: dead_tuples=%.0f, time_since_vacuum=%.0f ms, rate=%.2f/sec, base_thresh=%.0f, adj_thresh=%.0f, error=%.2f",
current_dead_tuples, time_since_last_vacuum_ms, dead_tuple_rate_per_sec, base_threshold, adjusted_threshold, error);
}
else
{
result.skip_table = true;
result.reason = pstrdup(psprintf("Custom policy: below adjusted threshold (%.0f < %.0f, rate=%.2f/sec)",
current_dead_tuples, adjusted_threshold, dead_tuple_rate_per_sec));
}

return result;
}

/* Hook function that calls the single policy */
static CustomAutovacuumPolicyResult
custom_autovacuum_policy_hook_func(CustomAutovacuumPolicyContext *context)
{
if (!custom_autovacuum_enabled)
return (CustomAutovacuumPolicyResult) {0};

return custom_autovacuum_policy(context);
}

/*
* Module initialization
*/
void
_PG_init(void)
{
/* Install the hook */
original_hook = custom_autovacuum_policy_hook;
custom_autovacuum_policy_hook = custom_autovacuum_policy_hook_func;

/* Define GUC variables */
DefineCustomBoolVariable("custom_autovacuum.enabled",
"Enable custom autovacuum policy",
NULL,
&custom_autovacuum_enabled,
true,
PGC_SIGHUP,
0,
NULL, NULL, NULL);

MarkGUCPrefixReserved("custom_autovacuum");
}
5 changes: 5 additions & 0 deletions contrib/custom_autovacuum/custom_autovacuum.control
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# custom_autovacuum extension
comment = 'Custom autovacuum policy framework'
default_version = '1.0'
module_pathname = '$libdir/custom_autovacuum'
relocatable = true
23 changes: 23 additions & 0 deletions contrib/custom_autovacuum/meson.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# contrib/custom_autovacuum/meson.build

custom_autovacuum_sources = files(
'custom_autovacuum.c',
)

custom_autovacuum = shared_module('custom_autovacuum',
custom_autovacuum_sources,
kwargs: contrib_mod_args,
)
contrib_targets += custom_autovacuum

install_data(
'custom_autovacuum.control',
'custom_autovacuum--1.0.sql',
kwargs: contrib_data_args,
)

tests += {
'name': 'custom_autovacuum',
'sd': meson.current_source_dir(),
'bd': meson.current_build_dir(),
}
Loading