Files
ubicloud/model/load_balancer.rb
Jeremy Evans fe2d670793 Avoid explicitly casting string arrays used for = ANY/!= ALL
This uses a new feature I developed for Sequel with Ubicloud in mind.
I found that explicit casts for array parameters used in this case
are not required, as long as PostgreSQL can implicitly cast elements of
the array to the type of the LHS of the expression.

Taking an example query from the query parameterization analysis:

```sql
DELETE FROM "applied_action_tag" WHERE (("tag_id" = $1) AND ("action_id" = ANY($2)))
```

In this case, $2 is a PostgreSQL array containing uuids.  As action_id is a
uuid type, PostgreSQL will assume $2 is uuid[], and things will work correctly.

It took me a while to figure this out, because PostgreSQL's behavior is
different when using ARRAY.  When not auto parameterizing, Sequel literalizes
arrays using ARRAY.  Initial work on auto parameterization used ARRAY and
parameterized the elements, and this let me to believe that explicitly casting
was necessary for arrays, when it turns out not to be.

As an example, this doesn't work (assuming $2 is a uuid string):

```sql
DELETE FROM "applied_action_tag" WHERE (("tag_id" = $1) AND ("action_id" = ANY(ARRAY[$2])))
```

For some reason, PostgreSQL thinks the array is text[] instead of
uuid[], even though the LHS is uuid.

The previous work around was to assume string arrays could be represented as
PostgreSQL text[] types.  I've used this successfully in other applications,
but in those, I wasn't using uuids or enum types.

As the majority of Ubicloud's usage in these cases is either uuid[] type or
enum array types, the assumption of text[] was not a good one for Ubicloud.
I previously added the Sequel.any_{type,uuid} methods to Ubicloud to make
it easier to fix the failing cases.  However, this is really a leaky
abstraction, and it's likely something that would trip up other developers.

By avoiding the explicit cast for string arrays used as parameters,
we can remove all usage of Sequel.any_{type,uuid} and have everything
work correctly.  This approach should be safe to run in all
environments.  I've made it so that the conversion is performed for
arrays with one or more elements only in frozen testing mode, since that is
how the query parameterization analysis is run and it can result in
a reduced number of distinct queries.  It can also catch potential issues
if the tests only test with single element arrays, but we are using multiple
element arrays in production.

By default, only arrays of two elements or more are use this conversion,
because PostgreSQL will use a more optimized query plan for a single element
value list than for a multiple element value list.

Over 10% of Ubicloud's distinct parameterized queries use this new feature
(126/1130).
2025-04-30 06:05:53 +09:00

180 lines
7.8 KiB
Ruby

# frozen_string_literal: true
require_relative "../model"
class LoadBalancer < Sequel::Model
many_to_one :project
many_to_many :vms
one_to_one :strand, key: :id
many_to_one :private_subnet
one_to_many :load_balancers_vms, key: :load_balancer_id, class: :LoadBalancersVms
one_to_many :ports, key: :load_balancer_id, class: :LoadBalancerPort
many_to_many :certs, join_table: :certs_load_balancers, left_key: :load_balancer_id, right_key: :cert_id
one_to_many :certs_load_balancers, key: :load_balancer_id, class: :CertsLoadBalancers
many_to_one :custom_hostname_dns_zone, class: :DnsZone, key: :custom_hostname_dns_zone_id
many_to_many :vm_ports, join_table: :load_balancer_port, right_key: :id, right_primary_key: :load_balancer_port_id, class: :LoadBalancerVmPort, read_only: true
many_to_many :active_vm_ports, join_table: :load_balancer_port, right_key: :id, right_primary_key: :load_balancer_port_id, class: :LoadBalancerVmPort, read_only: true, conditions: {state: "up"}
many_through_many :vms_to_dns, [[:load_balancer_port, :load_balancer_id, :id], [:load_balancer_vm_port, :load_balancer_port_id, :load_balancer_vm_id], [:load_balancers_vms, :id, :vm_id]], class: :Vm, conditions: Sequel.~(Sequel[:load_balancer_vm_port][:state] => ["evacuating", "detaching"])
plugin :association_dependencies, load_balancers_vms: :destroy, ports: :destroy, certs_load_balancers: :destroy
include ResourceMethods
include SemaphoreMethods
include ObjectTag::Cleanup
dataset_module Pagination
semaphore :destroy, :update_load_balancer, :rewrite_dns_records, :refresh_cert
def display_location
private_subnet.display_location
end
def path
"/location/#{private_subnet.display_location}/load-balancer/#{name}"
end
def vm_ports_by_vm(vm)
vm_ports_dataset.where(load_balancer_vm_id: load_balancers_vms_dataset.where(vm_id: vm.id).select(:id))
end
def vm_ports_by_vm_and_state(vm, state)
vm_ports_dataset.where(load_balancer_vm_id: load_balancers_vms_dataset.where(vm_id: vm.id).select(:id), state:)
end
def add_port(src_port, dst_port)
DB.transaction do
port = LoadBalancerPort.create(load_balancer_id: id, src_port:, dst_port:)
load_balancers_vms.each do |lb_vm|
LoadBalancerVmPort.create(load_balancer_port_id: port.id, load_balancer_vm_id: lb_vm.id)
end
incr_update_load_balancer
end
end
def remove_port(port)
DB.transaction do
vm_ports_dataset.where(load_balancer_port_id: port.id).destroy
port.destroy
incr_update_load_balancer
end
end
def add_vm(vm)
DB.transaction do
load_balancer_vm = LoadBalancersVms.create(load_balancer_id: id, vm_id: vm.id)
ports.each { |port|
LoadBalancerVmPort.create(load_balancer_port_id: port.id, load_balancer_vm_id: load_balancer_vm.id)
}
Strand.create_with_id(prog: "Vnet::CertServer", label: "put_certificate", stack: [{subject_id: id, vm_id: vm.id}], parent_id: id)
incr_rewrite_dns_records
end
end
def detach_vm(vm)
DB.transaction do
ids_to_update = vm_ports_by_vm_and_state(vm, ["up", "down", "evacuating"]).map(&:id)
LoadBalancerVmPort.where(id: ids_to_update).update(state: "detaching")
Strand.create_with_id(prog: "Vnet::CertServer", label: "remove_cert_server", stack: [{subject_id: id, vm_id: vm.id}], parent_id: id)
incr_update_load_balancer
end
end
def evacuate_vm(vm)
DB.transaction do
ids_to_update = vm_ports_by_vm_and_state(vm, ["up", "down"]).map(&:id)
LoadBalancerVmPort.where(id: ids_to_update).update(state: "evacuating")
Strand.create_with_id(prog: "Vnet::CertServer", label: "remove_cert_server", stack: [{subject_id: id, vm_id: vm.id}], parent_id: id)
incr_update_load_balancer
incr_rewrite_dns_records
end
end
def remove_vm(vm)
DB.transaction do
vm_ports_by_vm(vm).destroy
load_balancers_vms_dataset[vm_id: vm.id].destroy
incr_rewrite_dns_records
end
end
def remove_vm_port(vm_port)
DB.transaction do
LoadBalancerVmPort.where(id: vm_port.id).destroy
if vm_ports_dataset.where(load_balancer_vm_id: vm_port.load_balancer_vm_id).count.zero?
load_balancers_vms_dataset[id: vm_port.load_balancer_vm_id].destroy
end
incr_rewrite_dns_records
end
end
def hostname
custom_hostname || "#{name}.#{private_subnet.ubid[-5...]}.#{Config.load_balancer_service_hostname}"
end
def dns_zone
custom_hostname_dns_zone || DnsZone[project_id: Config.load_balancer_service_project_id, name: Config.load_balancer_service_hostname]
end
def need_certificates?
return true if certs_dataset.empty?
certs_dataset.where { created_at > Time.now - 60 * 60 * 24 * 30 * 2 }.exclude(cert: nil).empty?
end
def active_cert
certs_dataset.where { created_at > Time.now - 60 * 60 * 24 * 30 * 3 }.exclude(cert: nil).order(Sequel.desc(:created_at)).first
end
def ipv4_enabled?
stack == Stack::IPV4 || stack == Stack::DUAL
end
def ipv6_enabled?
stack == Stack::IPV6 || stack == Stack::DUAL
end
module Stack
IPV4 = "ipv4"
IPV6 = "ipv6"
DUAL = "dual"
end
end
# Table: load_balancer
# Columns:
# id | uuid | PRIMARY KEY
# name | text | NOT NULL
# algorithm | lb_algorithm | NOT NULL DEFAULT 'round_robin'::lb_algorithm
# private_subnet_id | uuid | NOT NULL
# health_check_endpoint | text | NOT NULL
# health_check_interval | integer | NOT NULL DEFAULT 10
# health_check_timeout | integer | NOT NULL DEFAULT 5
# health_check_up_threshold | integer | NOT NULL DEFAULT 5
# health_check_down_threshold | integer | NOT NULL DEFAULT 3
# health_check_protocol | lb_hc_protocol | NOT NULL DEFAULT 'http'::lb_hc_protocol
# custom_hostname | text |
# custom_hostname_dns_zone_id | uuid |
# stack | lb_stack | NOT NULL DEFAULT 'dual'::lb_stack
# project_id | uuid | NOT NULL
# Indexes:
# load_balancer_pkey | PRIMARY KEY btree (id)
# load_balancer_custom_hostname_key | UNIQUE btree (custom_hostname)
# load_balancer_private_subnet_id_name_uidx | UNIQUE btree (private_subnet_id, name)
# Check constraints:
# health_check_down_threshold_gt_0 | (health_check_down_threshold > 0)
# health_check_interval_gt_0 | (health_check_interval > 0)
# health_check_interval_lt_600 | (health_check_interval < 600)
# health_check_timeout_gt_0 | (health_check_timeout > 0)
# health_check_timeout_lt_health_check_interval | (health_check_timeout <= health_check_interval)
# health_check_up_threshold_gt_0 | (health_check_up_threshold > 0)
# Foreign key constraints:
# load_balancer_custom_hostname_dns_zone_id_fkey | (custom_hostname_dns_zone_id) REFERENCES dns_zone(id)
# load_balancer_private_subnet_id_fkey | (private_subnet_id) REFERENCES private_subnet(id)
# load_balancer_project_id_fkey | (project_id) REFERENCES project(id)
# Referenced By:
# certs_load_balancers | certs_load_balancers_load_balancer_id_fkey | (load_balancer_id) REFERENCES load_balancer(id)
# inference_endpoint | inference_endpoint_load_balancer_id_fkey | (load_balancer_id) REFERENCES load_balancer(id)
# inference_router | inference_router_load_balancer_id_fkey | (load_balancer_id) REFERENCES load_balancer(id)
# kubernetes_cluster | kubernetes_cluster_api_server_lb_id_fkey | (api_server_lb_id) REFERENCES load_balancer(id)
# load_balancer_port | load_balancer_port_load_balancer_id_fkey | (load_balancer_id) REFERENCES load_balancer(id)
# load_balancers_vms | load_balancers_vms_load_balancer_id_fkey | (load_balancer_id) REFERENCES load_balancer(id)