local config = { pg_database = "kong" } local Schema = require "kong.db.schema" local connector = require "kong.db.strategies.postgres.connector".new(config) describe("kong.db [#postgres] connector", function() describe(":infos()", function() it("returns infos db_ver always with two digit groups divided with dot (.)", function() local infos = connector.infos{ major_version = 9, major_minor_version = "9.5", config = config } assert.same({ db_desc = "database", db_ver = "9.5", strategy = "PostgreSQL", db_readonly = false, }, infos) local infos = connector.infos{ major_version = 9.5, major_minor_version = "9.5", config = config } assert.same({ db_desc = "database", db_ver = "9.5", strategy = "PostgreSQL", db_readonly = false, }, infos) infos = connector.infos{ major_version = 9, major_minor_version = "9.5.1", config = config } assert.same({ db_desc = "database", db_ver = "9.5", strategy = "PostgreSQL", db_readonly = false, }, infos) infos = connector.infos{ major_version = 9.5, major_minor_version = "9.5.1", config = config } assert.same({ db_desc = "database", db_ver = "9.5", strategy = "PostgreSQL", db_readonly = false, }, infos) infos = connector.infos{ major_version = 10, major_minor_version = "10.5", config = config } assert.same({ db_desc = "database", db_ver = "10.5", strategy = "PostgreSQL", db_readonly = false, }, infos) end) it("returns infos with db_ver as \"unknown\" when missing major_minor_version", function() local infos = connector.infos{ major_version = 9, config = config } assert.same({ db_desc = "database", db_ver = "unknown", strategy = "PostgreSQL", db_readonly = false, }, infos) infos = connector.infos{ major_version = 10, config = config } assert.same({ db_desc = "database", db_ver = "unknown", strategy = "PostgreSQL", db_readonly = false, }, infos) infos = connector.infos{ config = config } assert.same({ db_desc = "database", db_ver = "unknown", strategy = "PostgreSQL", db_readonly = false, }, infos) end) it("returns infos with db_ver as \"unknown\" when invalid major_minor_version", function() local infos = connector.infos{ major_version = 9, major_minor_version = "invalid", config = config } assert.same({ db_desc = "database", db_ver = "unknown", strategy = "PostgreSQL", db_readonly = false, }, infos) infos = connector.infos{ major_version = 10, major_minor_version = "invalid", config = config } assert.same({ db_desc = "database", db_ver = "unknown", strategy = "PostgreSQL", db_readonly = false, }, infos) infos = connector.infos{ major_minor_version = "invalid", config = config } assert.same({ db_desc = "database", db_ver = "unknown", strategy = "PostgreSQL", db_readonly = false, }, infos) end) it("returns db_readonly = true when readonly connection is enabled", function() local infos = connector.infos{ config = config, config_ro = config, } assert.same({ db_desc = "database", db_ver = "unknown", strategy = "PostgreSQL", db_readonly = true, }, infos) end) end) describe(":query() semaphore", function() describe("max 1", function() -- connector in a new scope local connector setup(function() local new_config = { pg_database = "kong", pg_max_concurrent_queries = 1, pg_semaphore_timeout = 1000, } connector = require "kong.db.strategies.postgres.connector".new(new_config) connector.get_stored_connection = function() return { query = function(_, s) ngx.sleep(s) end } end end) it("functions as a mutex", function() local errors = {} local co1 = ngx.thread.spawn(function() local _, err = connector:query(0.001) if err then table.insert(errors, err) end end) -- we are running in timer and semaphore needs some time to pass ngx.update_time() local co2 = ngx.thread.spawn(function() local _, err = connector:query(0.001) if err then table.insert(errors, err) end end) ngx.thread.wait(co2) ngx.thread.wait(co1) assert.same(0, #errors) end) it("times out failing to acquire a lock", function() local errors = {} local co1 = ngx.thread.spawn(function() local _, err = connector:query(1) if err then table.insert(errors, err) end end) local co2 = ngx.thread.spawn(function() local _, err = connector:query(0.1) if err then table.insert(errors, err) end end) ngx.thread.wait(co2) ngx.thread.wait(co1) assert.same(1, #errors) end) end) describe("max more than 1", function() -- connector in a new scope local connector setup(function() local new_config = { pg_database = "kong", pg_max_concurrent_queries = 2, pg_semaphore_timeout = 100, } connector = require "kong.db.strategies.postgres.connector".new(new_config) connector.get_stored_connection = function() return { query = function(_, s) ngx.sleep(s) end } end end) it("allows multiple functions to run concurrently", function() local errors = {} local co1 = ngx.thread.spawn(function() local _, err = connector:query(0.001) if err then table.insert(errors, err) end end) local co2 = ngx.thread.spawn(function() local _, err = connector:query(0.001) if err then table.insert(errors, err) end end) ngx.thread.wait(co2) ngx.thread.wait(co1) assert.same(0, #errors) end) it("times out failing to acquire a lock", function() local errors = {} local co1 = ngx.thread.spawn(function() local _, err = connector:query(1) if err then table.insert(errors, err) end end) local co2 = ngx.thread.spawn(function() local _, err = connector:query(0.1) if err then table.insert(errors, err) end end) local co3 = ngx.thread.spawn(function() local _, err = connector:query(0.1) if err then table.insert(errors, err) end end) ngx.thread.wait(co3) ngx.thread.wait(co2) ngx.thread.wait(co1) assert.same(1, #errors) end) end) end) describe("connector.get_topologically_sorted_table_names", function() local function schema_new(s) return { schema = assert(Schema.new(s)) } end local ts = connector._get_topologically_sorted_table_names it("prepends cluster_events no matter what", function() assert.same({"cluster_events", "clustering_rpc_requests"}, ts({})) end) it("sorts an array of unrelated schemas alphabetically by name", function() local a = schema_new({ name = "a", ttl = true, fields = {} }) local b = schema_new({ name = "b", ttl = true, fields = {} }) local c = schema_new({ name = "c", ttl = true, fields = {} }) assert.same({"cluster_events", "clustering_rpc_requests", "a", "b", "c"}, ts({ c, a, b })) end) it("ignores non-ttl schemas", function() local a = schema_new({ name = "a", ttl = true, fields = {} }) local b = schema_new({ name = "b", fields = {} }) local c = schema_new({ name = "c", ttl = true, fields = {} }) assert.same({"cluster_events", "clustering_rpc_requests", "a", "c"}, ts({ c, a, b })) end) it("it puts destinations first", function() local a = schema_new({ name = "a", ttl = true, fields = {} }) local c = schema_new({ name = "c", ttl = true, fields = { { a = { type = "foreign", reference = "a" }, }, } }) local b = schema_new({ name = "b", ttl = true, fields = { { a = { type = "foreign", reference = "a" }, }, { c = { type = "foreign", reference = "c" }, }, } }) assert.same({"cluster_events", "clustering_rpc_requests", "a", "c", "b"}, ts({ a, b, c })) end) it("puts core entities first, even when no relations", function() local a = schema_new({ name = "a", ttl = true, fields = {} }) local routes = schema_new({ name = "routes", ttl = true, fields = {} }) assert.same({"cluster_events", "clustering_rpc_requests", "routes", "a"}, ts({ a, routes })) end) it("puts workspaces before core and others, when no relations", function() local a = schema_new({ name = "a", ttl = true, fields = {} }) local workspaces = schema_new({ name = "workspaces", ttl = true, fields = {} }) local routes = schema_new({ name = "routes", ttl = true, fields = {} }) assert.same({"cluster_events", "clustering_rpc_requests", "workspaces", "routes", "a"}, ts({ a, routes, workspaces })) end) it("puts workspaces first, core entities second, and other entities afterwards, even with relations", function() local a = schema_new({ name = "a", ttl = true, fields = {} }) local services = schema_new({ name = "services", ttl = true, fields = {} }) local b = schema_new({ name = "b", ttl = true, fields = { { service = { type = "foreign", reference = "services" }, }, { a = { type = "foreign", reference = "a" }, }, } }) local routes = schema_new({ name = "routes", ttl = true, fields = { { service = { type = "foreign", reference = "services" }, }, } }) local workspaces = schema_new({ name = "workspaces", ttl = true, fields = {} }) assert.same({ "cluster_events", "clustering_rpc_requests", "workspaces", "services", "routes", "a", "b" }, ts({ services, b, a, workspaces, routes })) end) it("overrides core order if dependencies force it", function() -- This scenario is here in case in the future we allow plugin entities to precede core entities -- Not applicable today (kong 2.3.x) but maybe in future releases local a = schema_new({ name = "a", ttl = true, fields = {} }) local services = schema_new({ name = "services", ttl = true, fields = { { a = { type = "foreign", reference = "a" } } -- we somehow forced services to depend on a }}) local workspaces = schema_new({ name = "workspaces", ttl = true, fields = { { a = { type = "foreign", reference = "a" } } -- we somehow forced workspaces to depend on a } }) assert.same({ "cluster_events", "clustering_rpc_requests", "a", "workspaces", "services" }, ts({ services, a, workspaces })) end) it("returns an error if cycles are found", function() local a = schema_new({ name = "a", ttl = true, fields = { { b = { type = "foreign", reference = "b" }, }, } }) local b = schema_new({ name = "b", ttl = true, fields = { { a = { type = "foreign", reference = "a" }, }, } }) local x, err = ts({ a, b }) assert.is_nil(x) assert.equals("Cycle detected, cannot sort topologically", err) end) end) end)