Files
ubicloud/spec/model/postgres/postgres_timeline_spec.rb
Furkan Sahin 3c2bd3f04a Fix old backup and wal files delete policy
We were only cleaning up the basebackups and forget about the wal files.
With this commit, we start cleaning up the wal files, too.
2025-07-25 11:24:17 +03:00

238 lines
12 KiB
Ruby

# frozen_string_literal: true
require_relative "../spec_helper"
RSpec.describe PostgresTimeline do
subject(:postgres_timeline) { described_class.create_with_id(access_key: "dummy-access-key", secret_key: "dummy-secret-key", location_id: Location::HETZNER_FSN1_ID) }
it "returns ubid as bucket name" do
expect(postgres_timeline.bucket_name).to eq(postgres_timeline.ubid)
end
it "returns walg config" do
expect(postgres_timeline).to receive(:blob_storage).and_return(instance_double(MinioCluster, url: "https://blob-endpoint"))
walg_config = <<-WALG_CONF
WALG_S3_PREFIX=s3://#{postgres_timeline.ubid}
AWS_ENDPOINT=https://blob-endpoint
AWS_ACCESS_KEY_ID=dummy-access-key
AWS_SECRET_ACCESS_KEY=dummy-secret-key
AWS_REGION: us-east-1
AWS_S3_FORCE_PATH_STYLE=true
PGHOST=/var/run/postgresql
WALG_CONF
expect(postgres_timeline.generate_walg_config).to eq(walg_config)
expect(postgres_timeline).to receive(:aws?).and_return(true)
expect(postgres_timeline).to receive(:location).and_return(instance_double(Location, name: "us-east-2"))
expect(postgres_timeline.generate_walg_config).to eq(walg_config.sub("us-east-1", "us-east-2"))
end
describe "#need_backup?" do
let(:sshable) { instance_double(Sshable) }
let(:leader) {
instance_double(
PostgresServer,
strand: instance_double(Strand, label: "wait"),
vm: instance_double(Vm, sshable: sshable)
)
}
before do
allow(postgres_timeline).to receive(:leader).and_return(leader).at_least(:once)
end
it "returns false as backup needed if there is no backup endpoint is set" do
expect(postgres_timeline).to receive(:blob_storage).and_return(nil)
expect(postgres_timeline.need_backup?).to be(false)
end
it "returns false as backup needed if there is no leader" do
expect(postgres_timeline).to receive(:blob_storage).and_return("dummy-blob-storage")
expect(postgres_timeline).to receive(:leader).and_return(nil)
expect(postgres_timeline.need_backup?).to be(false)
end
it "returns true as backup needed if there is no backup process or the last backup failed" do
expect(postgres_timeline).to receive(:blob_storage).and_return("dummy-blob-storage").twice
expect(sshable).to receive(:cmd).and_return("NotStarted", "Failed")
expect(postgres_timeline.need_backup?).to be(true)
expect(postgres_timeline.need_backup?).to be(true)
end
it "returns true as backup needed if previous backup started more than a day ago and is succeeded" do
expect(postgres_timeline).to receive(:blob_storage).and_return("dummy-blob-storage")
expect(postgres_timeline).to receive(:latest_backup_started_at).and_return(Time.now - 60 * 60 * 25).twice
expect(sshable).to receive(:cmd).and_return("Succeeded")
expect(postgres_timeline.need_backup?).to be(true)
end
it "returns false as backup needed if previous backup started less than a day ago" do
expect(postgres_timeline).to receive(:blob_storage).and_return("dummy-blob-storage")
expect(postgres_timeline).to receive(:latest_backup_started_at).and_return(Time.now - 60 * 60 * 23).twice
expect(sshable).to receive(:cmd).and_return("Succeeded")
expect(postgres_timeline.need_backup?).to be(false)
end
it "returns false as backup needed if previous backup started is in progress" do
expect(postgres_timeline).to receive(:blob_storage).and_return("dummy-blob-storage")
expect(sshable).to receive(:cmd).and_return("InProgress")
expect(postgres_timeline.need_backup?).to be(false)
end
end
describe "#latest_backup_label_before_target" do
it "returns most recent backup before given target" do
most_recent_backup_time = Time.now
expect(postgres_timeline).to receive(:backups).and_return(
[
instance_double(Minio::Client::Blob, key: "basebackups_005/0001_backup_stop_sentinel.json", last_modified: most_recent_backup_time - 200),
instance_double(Minio::Client::Blob, key: "basebackups_005/0002_backup_stop_sentinel.json", last_modified: most_recent_backup_time - 100),
instance_double(Minio::Client::Blob, key: "basebackups_005/0003_backup_stop_sentinel.json", last_modified: most_recent_backup_time)
]
)
expect(postgres_timeline.latest_backup_label_before_target(target: most_recent_backup_time - 50)).to eq("0002")
end
it "raises error if no backups before given target" do
expect(postgres_timeline).to receive(:backups).and_return([])
expect { postgres_timeline.latest_backup_label_before_target(target: Time.now) }.to raise_error RuntimeError, "BUG: no backup found"
end
end
it "returns empty array if blob storage is not configured" do
expect(postgres_timeline).to receive(:blob_storage).and_return(nil)
expect(postgres_timeline.backups).to eq([])
end
it "returns empty array if user is not created yet" do
expect(postgres_timeline).to receive(:blob_storage).and_return(instance_double(MinioCluster, url: "https://blob-endpoint", root_certs: "certs")).at_least(:once)
minio_client = instance_double(Minio::Client)
expect(minio_client).to receive(:list_objects).and_raise(RuntimeError.new("The AWS Access Key Id you provided does not exist in our records."))
expect(Minio::Client).to receive(:new).and_return(minio_client)
expect(postgres_timeline.backups).to eq([])
end
it "re-raises exceptions other than missin access key" do
expect(postgres_timeline).to receive(:blob_storage).and_return(instance_double(MinioCluster, url: "https://blob-endpoint", root_certs: "certs")).at_least(:once)
minio_client = instance_double(Minio::Client)
expect(minio_client).to receive(:list_objects).and_raise(RuntimeError.new("some error"))
expect(Minio::Client).to receive(:new).and_return(minio_client)
expect { postgres_timeline.backups }.to raise_error(RuntimeError)
end
it "returns list of backups" do
expect(postgres_timeline).to receive(:blob_storage).and_return(instance_double(MinioCluster, url: "https://blob-endpoint", root_certs: "certs")).at_least(:once)
minio_client = Minio::Client.new(endpoint: "https://blob-endpoint", access_key: "access_key", secret_key: "secret_key", ssl_ca_data: "data")
expect(minio_client).to receive(:list_objects).with(postgres_timeline.ubid, "basebackups_005/").and_return([instance_double(Minio::Client::Blob, key: "backup_stop_sentinel.json"), instance_double(Minio::Client::Blob, key: "unrelated_file.txt")])
expect(Minio::Client).to receive(:new).and_return(minio_client)
expect(postgres_timeline.backups.map(&:key)).to eq(["backup_stop_sentinel.json"])
end
it "returns list of backups for AWS regions" do
expect(postgres_timeline).to receive(:location).and_return(instance_double(Location, aws?: true, name: "us-west-2")).at_least(:once)
s3_client = Aws::S3::Client.new(stub_responses: true)
s3_client.stub_responses(:list_objects_v2, {contents: [{key: "backup_stop_sentinel.json"}, {key: "unrelated_file.txt"}], is_truncated: false})
expect(s3_client).to receive(:list_objects_v2).with(bucket: postgres_timeline.ubid, prefix: "basebackups_005/").and_call_original
expect(Aws::S3::Client).to receive(:new).and_return(s3_client)
expect(postgres_timeline.backups.map(&:key)).to eq(["backup_stop_sentinel.json"])
end
it "returns list of backups with enumeration for AWS regions" do
expect(postgres_timeline).to receive(:location).and_return(instance_double(Location, aws?: true, name: "us-west-2")).at_least(:once)
s3_client = Aws::S3::Client.new(stub_responses: true)
s3_client.stub_responses(:list_objects_v2, {contents: [{key: "backup_stop_sentinel.json"}, {key: "unrelated_file.txt"}], is_truncated: true, next_continuation_token: "token"}, {contents: [{key: "backup_stop_sentinel.json"}, {key: "unrelated_file.txt"}], is_truncated: false})
expect(s3_client).to receive(:list_objects_v2).with(bucket: postgres_timeline.ubid, prefix: "basebackups_005/").and_call_original
expect(s3_client).to receive(:list_objects_v2).with(bucket: postgres_timeline.ubid, prefix: "basebackups_005/", continuation_token: "token").and_call_original
expect(Aws::S3::Client).to receive(:new).and_return(s3_client)
expect(postgres_timeline.backups.map(&:key)).to eq(["backup_stop_sentinel.json", "backup_stop_sentinel.json"])
end
it "returns blob storage endpoint" do
expect(MinioCluster).to receive(:[]).and_return(instance_double(MinioCluster, url: "https://blob-endpoint"))
expect(postgres_timeline.blob_storage_endpoint).to eq("https://blob-endpoint")
end
it "returns blob storage client from cache" do
expect(postgres_timeline).to receive(:blob_storage_endpoint).and_return("https://blob-endpoint")
expect(postgres_timeline).to receive(:blob_storage).and_return(instance_double(MinioCluster, root_certs: "certs")).once
expect(Minio::Client).to receive(:new).and_return("dummy-client").once
expect(postgres_timeline.blob_storage_client).to eq("dummy-client")
expect(postgres_timeline.blob_storage_client).to eq("dummy-client")
end
it "returns blob storage client when aws properly" do
expect(postgres_timeline).to receive(:location).and_return(nil)
expect(postgres_timeline).to receive(:blob_storage_endpoint).and_return("https://blob-endpoint")
expect(postgres_timeline).to receive(:blob_storage).and_return(instance_double(MinioCluster, root_certs: "certs")).once
expect(Minio::Client).to receive(:new).and_return("dummy-client").once
expect(postgres_timeline.blob_storage_client).to eq("dummy-client")
end
it "returns blob storage policy" do
policy = {Version: "2012-10-17", Statement: [{Effect: "Allow", Action: ["s3:*"], Resource: ["arn:aws:s3:::dummy-ubid*"]}]}
expect(postgres_timeline).to receive(:ubid).and_return("dummy-ubid")
expect(postgres_timeline.blob_storage_policy).to eq(policy)
end
it "returns earliest restore time" do
expect(postgres_timeline).to receive(:backups).and_return([instance_double(Minio::Client::Blob, last_modified: Time.now - 60 * 60 * 24 * 5)])
expect(postgres_timeline.earliest_restore_time.to_i).to be_within(5 * 60).of(Time.now.to_i - 60 * 60 * 24 * 5 + 5 * 60)
end
describe "aws" do
let(:s3_client) { Aws::S3::Client.new(stub_responses: true) }
before do
expect(postgres_timeline).to receive(:aws?).and_return(true).at_least(:once)
expect(Aws::S3::Client).to receive(:new).and_return(s3_client).at_least(:once)
end
it "creates bucket" do
expect(postgres_timeline).to receive(:location).and_return(instance_double(Location, name: "us-east-2")).at_least(:once)
s3_client.stub_responses(:create_bucket)
expect(s3_client).to receive(:create_bucket).with({bucket: postgres_timeline.ubid, create_bucket_configuration: {location_constraint: "us-east-2"}}).and_return(true)
expect(postgres_timeline.create_bucket).to be(true)
end
it "creates bucket in us-east-1" do
expect(postgres_timeline).to receive(:location).and_return(instance_double(Location, name: "us-east-1")).at_least(:once)
s3_client.stub_responses(:create_bucket)
expect(s3_client).to receive(:create_bucket).with({bucket: postgres_timeline.ubid, create_bucket_configuration: nil}).and_return(true)
expect(postgres_timeline.create_bucket).to be(true)
end
it "sets lifecycle policy" do
s3_client.stub_responses(:put_bucket_lifecycle_configuration)
expect(s3_client).to receive(:put_bucket_lifecycle_configuration).with({bucket: postgres_timeline.ubid, lifecycle_configuration: {rules: [{id: "DeleteOldBackups", status: "Enabled", expiration: {days: 8}, filter: {}}]}}).and_return(true)
expect(postgres_timeline.set_lifecycle_policy).to be(true)
end
end
describe "minio" do
let(:minio_client) { instance_double(Minio::Client) }
before do
expect(postgres_timeline).to receive(:aws?).and_return(false).at_least(:once)
expect(postgres_timeline).to receive(:blob_storage).and_return(instance_double(MinioCluster, url: "https://blob-endpoint", root_certs: "certs")).at_least(:once)
expect(Minio::Client).to receive(:new).and_return(minio_client).at_least(:once)
end
it "creates bucket" do
expect(minio_client).to receive(:create_bucket).with(postgres_timeline.ubid).and_return(true)
expect(postgres_timeline.create_bucket).to be(true)
end
it "sets lifecycle policy" do
expect(minio_client).to receive(:set_lifecycle_policy).with(postgres_timeline.ubid, postgres_timeline.ubid, 8).and_return(true)
expect(postgres_timeline.set_lifecycle_policy).to be(true)
end
end
end