From e3ce8281d3c6eb1cca6c5133f4d329f02ee5e9c0 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Thu, 29 Oct 2015 18:15:38 -0700 Subject: [PATCH 01/78] starting rewrite. Skeleton for load balancing and retry policies --- spec/integration/client_spec.lua | 18 +++++++++ src/cassandra/client.lua | 40 +++++++++++++++++++ src/cassandra/client_options.lua | 38 ++++++++++++++++++ src/cassandra/control_connection.lua | 52 ++++++++++++++++++++++++ src/cassandra/errors.lua | 60 ++++++++++++++++++++++++++++ src/cassandra/host.lua | 20 ++++++++++ src/cassandra/host_connection.lua | 58 +++++++++++++++++++++++++++ src/cassandra/log.lua | 26 ++++++++++++ src/cassandra/request_handler.lua | 31 ++++++++++++++ src/cassandra/requests.lua | 7 ++++ src/cassandra/session.lua | 2 +- src/cassandra/utils.lua | 17 ++++++++ 12 files changed, 368 insertions(+), 1 deletion(-) create mode 100644 spec/integration/client_spec.lua create mode 100644 src/cassandra/client.lua create mode 100644 src/cassandra/client_options.lua create mode 100644 src/cassandra/control_connection.lua create mode 100644 src/cassandra/errors.lua create mode 100644 src/cassandra/host.lua create mode 100644 src/cassandra/host_connection.lua create mode 100644 src/cassandra/log.lua create mode 100644 src/cassandra/request_handler.lua create mode 100644 src/cassandra/requests.lua diff --git a/spec/integration/client_spec.lua b/spec/integration/client_spec.lua new file mode 100644 index 0000000..36b778b --- /dev/null +++ b/spec/integration/client_spec.lua @@ -0,0 +1,18 @@ +local Client = require "cassandra.client" + +describe("Client", function() + it("should be instanciable", function() + assert.has_no_errors(function() + local client = Client({contact_points = {"127.0.0.1"}}) + assert.equal(false, client.connected) + end) + end) + describe("#execute()", function() + it("should return error if no host is available", function() + local client = Client({contact_points = {"0.0.0.1", "0.0.0.2", "0.0.0.3"}}) + local err = client:execute() + assert.truthy(err) + assert.equal("NoHostAvailableError", err.type) + end) + end) +end) diff --git a/src/cassandra/client.lua b/src/cassandra/client.lua new file mode 100644 index 0000000..da42d53 --- /dev/null +++ b/src/cassandra/client.lua @@ -0,0 +1,40 @@ +--- Responsible for a cluster of nodes +local Object = require "cassandra.classic" +local client_options = require "cassandra.client_options" +local ControlConnection = require "cassandra.control_connection" + +--- CLIENT +-- @section client + +local _CLIENT = Object:extend() + +function _CLIENT:new(options) + options = client_options.parse(options) + self.keyspace = options.keyspace + self.hosts = {} + self.connected = false + self.controlConnection = ControlConnection({contact_points = options.contact_points}) +end + +local function _connect(self) + if self.connected then return end + + local err + self.hosts, err = self.controlConnection:init() + local inspect = require "inspect" + print("Hosts: ", inspect(self.hosts)) + if err then + return err + end + + self.connected = true +end + +function _CLIENT:execute() + local err = _connect(self) + if err then + return err + end +end + +return _CLIENT diff --git a/src/cassandra/client_options.lua b/src/cassandra/client_options.lua new file mode 100644 index 0000000..4bc4bd7 --- /dev/null +++ b/src/cassandra/client_options.lua @@ -0,0 +1,38 @@ +local utils = require "cassandra.utils" +local errors = require "cassandra.errors" + +--- CONST +-- @section constants + +local DEFAULTS = { + contact_points = {}, + keyspace = "" +} + +local function parse(options) + if options == nil then options = {} end + + utils.extend_table(DEFAULTS, options) + + if type(options.contact_points) ~= "table" then + error("contact_points must be a table", 3) + end + + if not utils.is_array(options.contact_points) then + error("contact_points must be an array (integer-indexed table") + end + + if #options.contact_points < 1 then + error("contact_points must contain at least one contact point") + end + + if type(options.keyspace) ~= "string" then + error("keyspace must be a string") + end + + return options +end + +return { + parse = parse +} diff --git a/src/cassandra/control_connection.lua b/src/cassandra/control_connection.lua new file mode 100644 index 0000000..2782c6c --- /dev/null +++ b/src/cassandra/control_connection.lua @@ -0,0 +1,52 @@ +--- Represent a connection from the driver to the cluster and handle events between the two +local Object = require "cassandra.classic" +local Host = require "cassandra.host" +local HostConnection = require "cassandra.host_connection" +local RequestHandler = require "cassandra.request_handler" +local utils = require "cassandra.utils" +local log = require "cassandra.log" +local table_insert = table.insert + +--- Constants +-- @section constants + +local SELECT_PEERS_QUERY = "SELECT peer,data_center,rack,tokens,rpc_address,release_version FROM system.peers" +local SELECT_LOCAL_QUERY = "SELECT * FROM system.local WHERE key='local'" + +--- CONTROL_CONNECTION +-- @section control_connection + +local _CONTROL_CONNECTION = Object:extend() + +function _CONTROL_CONNECTION:new(options) + -- @TODO check attributes are valid (contact points, etc...) + self.hosts = {} + self.contact_points = options.contact_points + self.protocol_version = nil +end + +function _CONTROL_CONNECTION:init() + for _, contact_point in ipairs(self.contact_points) do + -- Extract port if string is of the form "host:port" + local addr, port = utils.split_by_colon(contact_point) + if not port then port = 9042 end -- @TODO add this to some constant + table_insert(self.hosts, Host(addr, port)) + end + + local any_host, err = RequestHandler.get_first_host(self.hosts) + if err then + return nil, err + end + -- @TODO get peers info + -- @TODO get local info + -- local peers, err + -- local local_infos, err + + return self.hosts +end + +function _CONTROL_CONNECTION:get_peers() + +end + +return _CONTROL_CONNECTION diff --git a/src/cassandra/errors.lua b/src/cassandra/errors.lua new file mode 100644 index 0000000..06f4748 --- /dev/null +++ b/src/cassandra/errors.lua @@ -0,0 +1,60 @@ +local type = type +local tostring = tostring +local string_format = string.format + +--- CONST +-- @section constants + +local ERROR_TYPES = { + NoHostAvailableError = { + info = "Represents an error when a query cannot be performed because no host is available or could be reached by the driver.", + message = function(errors) + if type(errors) ~= "table" then + error("NoHostAvailableError must be given a list of errors") + end + + local message = "All hosts tried for query failed." + for address, err in pairs(errors) do + message = string_format("%s %s: %s.", message, address, err) + end + return message + end + } +} + +--- ERROR_MT +-- @section error_mt + +local _error_mt = {} +_error_mt.__index = _error_mt + +function _error_mt:__tostring() + return tostring(string_format("%s: %s", self.type, self.message)) +end + +function _error_mt.__concat(a, b) + if getmetatable(a) == _error_mt then + return tostring(a)..b + else + return a..tostring(b) + end +end + +--- _ERRORS +-- @section _errors + +local _ERRORS = {} + +for k, v in pairs(ERROR_TYPES) do + _ERRORS[k] = function(message) + local err = { + type = k, + info = v.info, + message = type(v.message) == "function" and v.message(message) or message + } + + return setmetatable(err, error_mt) + end +end + +return _ERRORS diff --git a/src/cassandra/host.lua b/src/cassandra/host.lua new file mode 100644 index 0000000..28d4032 --- /dev/null +++ b/src/cassandra/host.lua @@ -0,0 +1,20 @@ +--- Represent one Cassandra node +local Object = require "cassandra.classic" +local HostConnection = require "cassandra.host_connection" +local string_find = string.find + +--- _HOST +-- @section host + +local _HOST = Object:extend() + +function _HOST:new(host, port) + self.address = host..":"..port + self.casandra_version = nil + self.datacenter = nil + self.rack = nil + self.unhealthy_at = 0 + self.connection = HostConnection(host, port) +end + +return _HOST diff --git a/src/cassandra/host_connection.lua b/src/cassandra/host_connection.lua new file mode 100644 index 0000000..064ea7f --- /dev/null +++ b/src/cassandra/host_connection.lua @@ -0,0 +1,58 @@ +--- Represent one socket to connect to a Cassandra node +local Object = require "cassandra.classic" + +--- Constants +-- @section constants + +local SOCKET_TYPES = { + NGX = "ngx", + LUASOCKET = "luasocket" +} + +--- Utils +-- @section utils + +local function new_socket() + local tcp_sock, sock_type + + if ngx and ngx.get_phase ~= nil and ngx.get_phase ~= "init" then + -- lua-nginx-module + tcp_sock = ngx.socket.tcp + sock_type = SOCKET_TYPES.NGX + else + -- fallback to luasocket + tcp_sock = require("socket").tcp + sock_type = SOCKET_TYPES.LUASOCKET + end + + local socket, err = tcp_sock() + if not socket then + return nil, err + end + + return socket, sock_type +end + +--- _HOST_CONNECTION +-- @section host_connection + +local _HOST_CONNECTION = Object:extend() + +function _HOST_CONNECTION:new(host, port) + local socket, socket_type = new_socket() + self.host = host + self.port = port + self.socket = socket + self.socket_type = socket_type +end + +local function startup() + +end + +function _HOST_CONNECTION:open() + local ok, err = self.socket:connect(self.host, self.port) + return ok == 1, err +end + +return _HOST_CONNECTION diff --git a/src/cassandra/log.lua b/src/cassandra/log.lua new file mode 100644 index 0000000..89d0ec0 --- /dev/null +++ b/src/cassandra/log.lua @@ -0,0 +1,26 @@ +local string_format = string.format + +local LEVELS = { + "ERR", + "INFO", + "DEBUG" +} + +local _LOG = {} + +local function log(level, message) + if ngx and type(ngx.log) == "function" then + -- lua-nginx-module + ngx.log(ngx[level], message) + else + print(string_format("%s: %s", level, message)) -- can't configure level for now + end +end + +for _, level in ipairs(LEVELS) do + _LOG[level:lower()] = function(message) + log(level, message) + end +end + +return _LOG diff --git a/src/cassandra/request_handler.lua b/src/cassandra/request_handler.lua new file mode 100644 index 0000000..24a6eab --- /dev/null +++ b/src/cassandra/request_handler.lua @@ -0,0 +1,31 @@ +local Object = require "cassandra.classic" +local Errors = require "cassandra.errors" + +--- _REQUEST_HANDLER +-- @section request_handler + +local _REQUEST_HANDLER = Object:extend() + +function _REQUEST_HANDLER:mew(options) + self.loadBalancingPolicy = nil -- @TODO + self.retryPolicy = nil -- @TODO + self.request = options.request + self.host = options.host +end + +-- Get the first connection from the available one with no regards for the load balancing policy +function _REQUEST_HANDLER.get_first_host(hosts) + local errors = {} + for _, host in ipairs(hosts) do + local connected, err = host.connection:open() + if not connected then + errors[host.address] = err + else + return host + end + end + + return nil, Errors.NoHostAvailableError(errors) +end + +return _REQUEST_HANDLER diff --git a/src/cassandra/requests.lua b/src/cassandra/requests.lua new file mode 100644 index 0000000..50cf6a4 --- /dev/null +++ b/src/cassandra/requests.lua @@ -0,0 +1,7 @@ +local _REQUESTS = {} + +function _REQUESTS.startup(cql_version) + +end + +return _REQUESTS diff --git a/src/cassandra/session.lua b/src/cassandra/session.lua index 18a7d8a..dd31936 100644 --- a/src/cassandra/session.lua +++ b/src/cassandra/session.lua @@ -91,7 +91,7 @@ function _M:connect(contact_points, port, options) error("no contact points provided", 2) elseif type(contact_points) == "table" then -- shuffle the contact points so we don't try to always connect on the same order, - -- avoiding pressure on the same node cordinator. + -- avoiding pressure on the same node coordinator. contact_points = utils.shuffle_array(contact_points) else contact_points = {contact_points} diff --git a/src/cassandra/utils.lua b/src/cassandra/utils.lua index 452c8f7..43d10e1 100644 --- a/src/cassandra/utils.lua +++ b/src/cassandra/utils.lua @@ -28,6 +28,23 @@ function _M.shuffle_array(arr) return arr end +function _M.extend_table(defaults, values) + for k in pairs(defaults) do + if values[k] == nil then + values[k] = defaults[k] + end + end +end + +function _M.is_array(t) + local i = 0 + for _ in pairs(t) do + i = i + 1 + if t[i] == nil and t[tostring(i)] == nil then return false end + end + return true +end + function _M.split_by_colon(str) local fields = {} str:gsub("([^:]+)", function(c) fields[#fields+1] = c end) From 8fd0995b1d3c7e1a7603a3f17c2745fa9dec2204 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Fri, 30 Oct 2015 20:40:06 -0700 Subject: [PATCH 02/78] feat(rewrite) buffer/serializers and frame writers --- spec/buffer_spec.lua | 34 +++++++++++++ spec/requests_spec.lua | 49 ++++++++++++++++++ spec/utils_spec.lua | 48 ++++++++++++++++++ src/cassandra/buffer.lua | 18 +++++++ src/cassandra/consts.lua | 6 +++ src/cassandra/host_connection.lua | 1 + src/cassandra/requests.lua | 45 +++++++++++++++-- src/cassandra/types/boolean.lua | 12 +++++ src/cassandra/types/byte.lua | 11 ++++ src/cassandra/types/frame_header.lua | 75 ++++++++++++++++++++++++++++ src/cassandra/types/integer.lua | 11 ++++ src/cassandra/types/short.lua | 11 ++++ src/cassandra/types/string.lua | 10 ++++ src/cassandra/types/string_map.lua | 23 +++++++++ src/cassandra/utils.lua | 36 +++++++++++++ src/cassandra/utils/buffer.lua | 31 ++++++++++++ 16 files changed, 418 insertions(+), 3 deletions(-) create mode 100644 spec/buffer_spec.lua create mode 100644 spec/requests_spec.lua create mode 100644 spec/utils_spec.lua create mode 100644 src/cassandra/buffer.lua create mode 100644 src/cassandra/consts.lua create mode 100644 src/cassandra/types/boolean.lua create mode 100644 src/cassandra/types/byte.lua create mode 100644 src/cassandra/types/frame_header.lua create mode 100644 src/cassandra/types/integer.lua create mode 100644 src/cassandra/types/short.lua create mode 100644 src/cassandra/types/string.lua create mode 100644 src/cassandra/types/string_map.lua create mode 100644 src/cassandra/utils/buffer.lua diff --git a/spec/buffer_spec.lua b/spec/buffer_spec.lua new file mode 100644 index 0000000..72ab619 --- /dev/null +++ b/spec/buffer_spec.lua @@ -0,0 +1,34 @@ +local Buffer = require "cassandra.buffer" + +describe("Types", function() + local FIXTURES = { + short = {0, 1, -1, 12, 13}, + byte = {1, 2, 3}, + boolean = {true, false}, + integer = {0, 4200, -42}, + string = {"hello world"}, + string_map = { + {hello = "world"}, + {cql_version = "3.0.0", foo = "bar"} + } + } + + for fixture_type, fixture_values in pairs(FIXTURES) do + it("["..fixture_type.."] should be bufferable", function() + for _, fixture in ipairs(fixture_values) do + local writer = Buffer() + writer["write_"..fixture_type](writer, fixture) + local bytes = writer:write() + + local reader = Buffer(bytes) + local decoded = reader["read_"..fixture_type](reader) + + if type(fixture) == "table" then + assert.same(fixture, decoded) + else + assert.equal(fixture, decoded) + end + end + end) + end +end) diff --git a/spec/requests_spec.lua b/spec/requests_spec.lua new file mode 100644 index 0000000..dbbf1a9 --- /dev/null +++ b/spec/requests_spec.lua @@ -0,0 +1,49 @@ +local CONSTS = require "cassandra.consts" +local requests = require "cassandra.requests" +local Buffer = require "cassandra.buffer" +local frame_header = require "cassandra.types.frame_header" + +local op_codes = frame_header.op_codes + +describe("Requests", function() + local Request = requests.Request + describe("Request", function() + it("should write its own frame", function() + local buffer = Buffer() + buffer:write_byte(0x03) + buffer:write_byte(0) -- flags + buffer:write_byte(0) -- stream id + buffer:write_byte(op_codes.STARTUP) + buffer:write_integer(0) -- body length + + local req = Request({op_code = op_codes.STARTUP}) + assert.equal(buffer:write(), req:write()) + end) + it("should proxy all writer functions", function() + local buffer = Buffer() + buffer:write_byte(0x03) + buffer:write_byte(0) -- flags + buffer:write_byte(0) -- stream id + buffer:write_byte(op_codes.STARTUP) + buffer:write_integer(22) -- body length + buffer:write_string_map({CQL_VERSION = "3.0.0"}) + + local req = Request({op_code = op_codes.STARTUP}) + assert.has_no_errors(function() + req:write_string_map({CQL_VERSION = "3.0.0"}) + end) + + assert.equal(buffer:write(), req:write()) + end) + end) + describe("StartupRequest", function() + it("should write a startup request", function() + local req = Request({op_code = op_codes.STARTUP}) + req:write_string_map({CQL_VERSION = "3.0.0"}) + + local startup = requests.StartupRequest() + + assert.equal(req:write(), startup:write()) + end) + end) +end) diff --git a/spec/utils_spec.lua b/spec/utils_spec.lua new file mode 100644 index 0000000..d81b3dd --- /dev/null +++ b/spec/utils_spec.lua @@ -0,0 +1,48 @@ +local utils = require "cassandra.utils" + +describe("utils", function() + describe("const_mt", function() + + local VERSION_CODES = { + [2] = { + REQUEST = 20, + RESPONSE = 21, + SOME_V2 = 2222 + }, + [3] = { + REQUEST = 30, + RESPONSE = 31, + SOME_V3_ONLY = 3333 + } + } + setmetatable(VERSION_CODES, utils.const_mt) + + local FLAGS = { + COMPRESSION = 1, + TRACING = 2, + [4] = { + CUSTOM_PAYLOAD = 4 + } + } + setmetatable(FLAGS, utils.const_mt) + + describe("#get()", function() + it("should get most recent version of a constant", function() + assert.equal(30, VERSION_CODES:get("REQUEST")) + assert.equal(31, VERSION_CODES:get("RESPONSE")) + assert.equal(3333, VERSION_CODES:get("SOME_V3_ONLY")) + assert.equal(2222, VERSION_CODES:get("SOME_V2")) + end) + it("should get constant from the root", function() + assert.equal(1, FLAGS:get("COMPRESSION")) + assert.equal(2, FLAGS:get("TRACING")) + end) + it("should accept a version parameter for which version to look into", function() + assert.equal(4, FLAGS:get("CUSTOM_PAYLOAD", 4)) + assert.equal(20, VERSION_CODES:get("REQUEST", 2)) + assert.equal(21, VERSION_CODES:get("RESPONSE", 2)) + end) + end) + + end) +end) diff --git a/src/cassandra/buffer.lua b/src/cassandra/buffer.lua new file mode 100644 index 0000000..7fba0c1 --- /dev/null +++ b/src/cassandra/buffer.lua @@ -0,0 +1,18 @@ +local Buffer = require "cassandra.utils.buffer" + +local TYPES = { + "byte", + "short", + "boolean", + "integer", + "string", + "string_map" +} + +for _, buf_type in ipairs(TYPES) do + local mod = require("cassandra.types."..buf_type) + Buffer["read_"..buf_type] = mod.read + Buffer["write_"..buf_type] = mod.write +end + +return Buffer diff --git a/src/cassandra/consts.lua b/src/cassandra/consts.lua new file mode 100644 index 0000000..660e516 --- /dev/null +++ b/src/cassandra/consts.lua @@ -0,0 +1,6 @@ +return { + DEFAULT_PROTOCOL_VERSION = 3, + MIN_PROTOCOL_VERSION = 2, + MAX_PROTOCOL_VERSION = 3, + CQL_VERSION = "3.0.0" +} diff --git a/src/cassandra/host_connection.lua b/src/cassandra/host_connection.lua index 064ea7f..e51d6a7 100644 --- a/src/cassandra/host_connection.lua +++ b/src/cassandra/host_connection.lua @@ -1,5 +1,6 @@ --- Represent one socket to connect to a Cassandra node local Object = require "cassandra.classic" +local CONSTS = require "cassandra.consts" --- Constants -- @section constants diff --git a/src/cassandra/requests.lua b/src/cassandra/requests.lua index 50cf6a4..9238773 100644 --- a/src/cassandra/requests.lua +++ b/src/cassandra/requests.lua @@ -1,7 +1,46 @@ -local _REQUESTS = {} +local CONSTS = require "cassandra.consts" +local Buffer = require "cassandra.buffer" +local frame_header = require "cassandra.types.frame_header" -function _REQUESTS.startup(cql_version) +local op_codes = frame_header.op_codes +local FrameHeader = frame_header.FrameHeader +--- Request +-- @section request + +local Request = Buffer:extend() + +function Request:new(options) + if options == nil then options = {} end + + self.version = options.version and options.version or CONSTS.DEFAULT_PROTOCOL_VERSION + self.op_code = options.op_code + + Request.super.new(self, nil, self.version) +end + +function Request:write(flags) + if not self.op_code then error("Request#write() has no op_code") end + + local frameHeader = FrameHeader(self.version, flags, self.op_code, self.len) + return frameHeader:write()..Request.super.write(self) +end + +--- StartupRequest +-- @section startup_request + +local StartupRequest = Request:extend() + +function StartupRequest:new(...) + StartupRequest.super.new(self, ...) + + self.op_code = op_codes.STARTUP + StartupRequest.super.write_string_map(self, { + CQL_VERSION = CONSTS.CQL_VERSION + }) end -return _REQUESTS +return { + Request = Request, + StartupRequest = StartupRequest +} diff --git a/src/cassandra/types/boolean.lua b/src/cassandra/types/boolean.lua new file mode 100644 index 0000000..da61611 --- /dev/null +++ b/src/cassandra/types/boolean.lua @@ -0,0 +1,12 @@ +return { + write = function(self, val) + if val then + self:write_byte(1) + else + self:write_byte(0) + end + end, + read = function(self) + return self:read_byte() == 1 + end +} diff --git a/src/cassandra/types/byte.lua b/src/cassandra/types/byte.lua new file mode 100644 index 0000000..220bfc1 --- /dev/null +++ b/src/cassandra/types/byte.lua @@ -0,0 +1,11 @@ +local utils = require "cassandra.utils" + +return { + write = function(self, val) + self:write_bytes(utils.big_endian_representation(val, 1)) + end, + read = function(self) + local byte = self:read_bytes(1) + return utils.string_to_number(byte, true) + end +} diff --git a/src/cassandra/types/frame_header.lua b/src/cassandra/types/frame_header.lua new file mode 100644 index 0000000..f34a529 --- /dev/null +++ b/src/cassandra/types/frame_header.lua @@ -0,0 +1,75 @@ +local utils = require "cassandra.utils" +local Buffer = require "cassandra.buffer" + +--- CONST +-- @section constants + +local VERSION_CODES = { + [2] = { + REQUEST = 0x02, + RESPONSE = 0x82 + }, + [3] = { + REQUEST = 0x03, + RESPONSE = 0x83 + } +} + +setmetatable(VERSION_CODES, utils.const_mt) + +local FLAGS = { + COMPRESSION = 0x01, -- not implemented + TRACING = 0x02 +} + +setmetatable(FLAGS, utils.const_mt) + +local OP_CODES = { + ERROR = 0x00, + STARTUP = 0x01, + READY = 0x02, + AUTHENTICATE = 0x03, + OPTIONS = 0x05, + SUPPORTED = 0x06, + QUERY = 0x07, + RESULT = 0x08, + PREPARE = 0x09, + EXECUTE = 0x0A, + REGISTER = 0x0B, + EVENT = 0x0C, + BATCH = 0x0D, + AUTH_CHALLENGE = 0x0E, + AUTH_RESPONSE = 0x0F, + AUTH_SUCCESS = 0x10 +} + +setmetatable(OP_CODES, utils.const_mt) + +--- FrameHeader +-- @section FrameHeader + +local FrameHeader = Buffer:extend() + +function FrameHeader:new(version, flags, op_code, body_length) + self.flags = flags and flags or 0 + self.op_code = op_code + self.body_length = body_length + + self.super.new(self, nil, version) +end + +function FrameHeader:write() + self.super.write_byte(self, VERSION_CODES[self.version].REQUEST) + self.super.write_byte(self, self.flags) -- @TODO make sure to expose flags to the client or find a more secure way + self.super.write_byte(self, 0) -- @TODO support streaming + self.super.write_byte(self, self.op_code) -- @TODO make sure to expose op_codes to the client or find a more secure way + self.super.write_integer(self, self.body_length) + + return self.super.write(self) +end + +return { + op_codes = OP_CODES, + flags = FLAGS, + FrameHeader = FrameHeader +} diff --git a/src/cassandra/types/integer.lua b/src/cassandra/types/integer.lua new file mode 100644 index 0000000..605deec --- /dev/null +++ b/src/cassandra/types/integer.lua @@ -0,0 +1,11 @@ +local utils = require "cassandra.utils" + +return { + read = function(self) + local bytes = self:read_bytes(4) + return utils.string_to_number(bytes, true) + end, + write = function(self, val) + self:write_bytes(utils.big_endian_representation(val, 4)) + end +} diff --git a/src/cassandra/types/short.lua b/src/cassandra/types/short.lua new file mode 100644 index 0000000..fd9c35a --- /dev/null +++ b/src/cassandra/types/short.lua @@ -0,0 +1,11 @@ +local utils = require "cassandra.utils" + +return { + write = function(self, val) + self:write_bytes(utils.big_endian_representation(val, 2)) + end, + read = function(self) + local bytes = self:read_bytes(2) + return utils.string_to_number(bytes, true) + end +} diff --git a/src/cassandra/types/string.lua b/src/cassandra/types/string.lua new file mode 100644 index 0000000..d49e004 --- /dev/null +++ b/src/cassandra/types/string.lua @@ -0,0 +1,10 @@ +return { + write = function(self, str) + self:write_short(#str) + self:write_bytes(str) + end, + read = function(self) + local n_bytes = self:read_short() + return self:read_bytes(n_bytes) + end +} diff --git a/src/cassandra/types/string_map.lua b/src/cassandra/types/string_map.lua new file mode 100644 index 0000000..a9e9f88 --- /dev/null +++ b/src/cassandra/types/string_map.lua @@ -0,0 +1,23 @@ +return { + write = function(self, map) + local n = #map + for k, v in pairs(map) do + n = n + 1 + end + self:write_short(n) + for k, v in pairs(map) do + self:write_string(k) + self:write_string(v) + end + end, + read = function(self) + local map = {} + local n_strings = self:read_short() + for _ = 1, n_strings do + local key = self:read_string() + local value = self:read_string() + map[key] = value + end + return map + end +} diff --git a/src/cassandra/utils.lua b/src/cassandra/utils.lua index 43d10e1..b56ec79 100644 --- a/src/cassandra/utils.lua +++ b/src/cassandra/utils.lua @@ -1,3 +1,5 @@ +local CONSTS = require "cassandra.consts" + local _M = {} function _M.big_endian_representation(num, bytes) @@ -15,6 +17,20 @@ function _M.big_endian_representation(num, bytes) return padding .. table.concat(t) end +function _M.string_to_number(str, signed) + local number = 0 + local exponent = 1 + for i = #str, 1, -1 do + number = number + string.byte(str, i) * exponent + exponent = exponent * 256 + end + if signed and number > exponent / 2 then + -- 2's complement + number = number - exponent + end + return number +end + math.randomseed(os.time()) -- @see http://en.wikipedia.org/wiki/Fisher-Yates_shuffle @@ -73,4 +89,24 @@ function _M.deep_copy(orig) return copy end +local rawget = rawget + +local _const_mt = { + get = function(t, key, version) + if not version then version = CONSTS.MAX_PROTOCOL_VERSION end + + local const, version_consts + while version >= CONSTS.MIN_PROTOCOL_VERSION and const == nil do + version_consts = t[version] ~= nil and t[version] or t + const = rawget(version_consts, key) + version = version - 1 + end + return const + end +} + +_const_mt.__index = _const_mt + +_M.const_mt = _const_mt + return _M diff --git a/src/cassandra/utils/buffer.lua b/src/cassandra/utils/buffer.lua new file mode 100644 index 0000000..cc465ba --- /dev/null +++ b/src/cassandra/utils/buffer.lua @@ -0,0 +1,31 @@ +local Object = require "cassandra.classic" +local string_sub = string.sub +local table_insert = table.insert +local table_concat = table.concat + +local Buffer = Object:extend() + +function Buffer:new(str, version) + self.version = version -- protocol version + self.str = str and str or "" + self.pos = 1 -- lua indexes start at 1, remember? + self.len = #self.str +end + +function Buffer:write() + return self.str +end + +function Buffer:write_bytes(value) + self.str = self.str..value + self.len = self.len + #value + self.pos = self.len +end + +function Buffer:read_bytes(n_bytes_to_read) + local last_index = n_bytes_to_read ~= nil and self.pos + n_bytes_to_read - 1 or -1 + local bytes = string_sub(self.str, self.pos, last_index) + self.pos = self.pos + #bytes return bytes +end + +return Buffer From f6589c6977452f272035c3c0fbbae69a51f7ca90 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Sun, 1 Nov 2015 17:42:58 -0800 Subject: [PATCH 03/78] chore(rewrite) isolate rewrite tests --- .../client_spec.lua | 6 ++++++ spec/{ => rewrite_unit}/buffer_spec.lua | 14 +++++++++++++- spec/{ => rewrite_unit}/requests_spec.lua | 0 spec/{ => rewrite_unit}/utils_spec.lua | 0 src/cassandra/types/byte.lua | 7 ++++--- src/cassandra/utils/buffer.lua | 4 ++++ 6 files changed, 27 insertions(+), 4 deletions(-) rename spec/{integration => rewrite_integration}/client_spec.lua (77%) rename spec/{ => rewrite_unit}/buffer_spec.lua (69%) rename spec/{ => rewrite_unit}/requests_spec.lua (100%) rename spec/{ => rewrite_unit}/utils_spec.lua (100%) diff --git a/spec/integration/client_spec.lua b/spec/rewrite_integration/client_spec.lua similarity index 77% rename from spec/integration/client_spec.lua rename to spec/rewrite_integration/client_spec.lua index 36b778b..64842bb 100644 --- a/spec/integration/client_spec.lua +++ b/spec/rewrite_integration/client_spec.lua @@ -1,5 +1,7 @@ local Client = require "cassandra.client" +local ONE_NODE_CLUSTER = {"127.0.0.1"} + describe("Client", function() it("should be instanciable", function() assert.has_no_errors(function() @@ -14,5 +16,9 @@ describe("Client", function() assert.truthy(err) assert.equal("NoHostAvailableError", err.type) end) + it("TODO", function() + local client = Client({contact_points = ONE_NODE_CLUSTER}) + local err = client:execute() + end) end) end) diff --git a/spec/buffer_spec.lua b/spec/rewrite_unit/buffer_spec.lua similarity index 69% rename from spec/buffer_spec.lua rename to spec/rewrite_unit/buffer_spec.lua index 72ab619..3629167 100644 --- a/spec/buffer_spec.lua +++ b/spec/rewrite_unit/buffer_spec.lua @@ -1,6 +1,6 @@ local Buffer = require "cassandra.buffer" -describe("Types", function() +describe("Buffer", function() local FIXTURES = { short = {0, 1, -1, 12, 13}, byte = {1, 2, 3}, @@ -31,4 +31,16 @@ describe("Types", function() end end) end + + it("should accumulate values", function() + local writer = Buffer() + writer:write_byte(2) + writer:write_integer(128) + writer:write_string("hello world") + + local reader = Buffer.from_buffer(writer) + assert.equal(2, reader:read_byte()) + assert.equal(128, reader:read_integer()) + assert.equal("hello world", reader:read_string()) + end) end) diff --git a/spec/requests_spec.lua b/spec/rewrite_unit/requests_spec.lua similarity index 100% rename from spec/requests_spec.lua rename to spec/rewrite_unit/requests_spec.lua diff --git a/spec/utils_spec.lua b/spec/rewrite_unit/utils_spec.lua similarity index 100% rename from spec/utils_spec.lua rename to spec/rewrite_unit/utils_spec.lua diff --git a/src/cassandra/types/byte.lua b/src/cassandra/types/byte.lua index 220bfc1..a3489ee 100644 --- a/src/cassandra/types/byte.lua +++ b/src/cassandra/types/byte.lua @@ -1,11 +1,12 @@ -local utils = require "cassandra.utils" +local string_char = string.char +local string_byte = string.byte return { write = function(self, val) - self:write_bytes(utils.big_endian_representation(val, 1)) + self:write_bytes(string_char(val)) end, read = function(self) local byte = self:read_bytes(1) - return utils.string_to_number(byte, true) + return string_byte(byte) end } diff --git a/src/cassandra/utils/buffer.lua b/src/cassandra/utils/buffer.lua index cc465ba..9edd947 100644 --- a/src/cassandra/utils/buffer.lua +++ b/src/cassandra/utils/buffer.lua @@ -28,4 +28,8 @@ function Buffer:read_bytes(n_bytes_to_read) self.pos = self.pos + #bytes return bytes end +function Buffer.from_buffer(buffer) + return Buffer(buffer:write(), buffer.version) +end + return Buffer From 13216390f66c3b6050611bef1c4a19efaec79c09 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Sun, 1 Nov 2015 19:30:48 -0800 Subject: [PATCH 04/78] feat(rewrite) implement startup flow --- lua-cassandra-0.3.6-0.rockspec | 3 + spec/rewrite_integration/client_spec.lua | 5 +- spec/rewrite_unit/requests_spec.lua | 16 ++++- src/cassandra/control_connection.lua | 11 ++- src/cassandra/errors.lua | 12 +++- src/cassandra/frame_reader.lua | 88 ++++++++++++++++++++++++ src/cassandra/host.lua | 12 ++-- src/cassandra/host_connection.lua | 85 ++++++++++++++++++++--- src/cassandra/request_handler.lua | 11 +-- src/cassandra/requests.lua | 10 ++- src/cassandra/types/frame_header.lua | 33 +++++++-- src/cassandra/utils/buffer.lua | 9 +-- 12 files changed, 242 insertions(+), 53 deletions(-) create mode 100644 src/cassandra/frame_reader.lua diff --git a/lua-cassandra-0.3.6-0.rockspec b/lua-cassandra-0.3.6-0.rockspec index 6823313..a7648d7 100644 --- a/lua-cassandra-0.3.6-0.rockspec +++ b/lua-cassandra-0.3.6-0.rockspec @@ -9,6 +9,9 @@ description = { homepage = "http://thibaultcha.github.io/lua-cassandra", license = "MIT" } +dependencies = { + "luabitop ~> 1.0.2-2" +} build = { type = "builtin", modules = { diff --git a/spec/rewrite_integration/client_spec.lua b/spec/rewrite_integration/client_spec.lua index 64842bb..cc23f02 100644 --- a/spec/rewrite_integration/client_spec.lua +++ b/spec/rewrite_integration/client_spec.lua @@ -1,17 +1,18 @@ local Client = require "cassandra.client" +local FAKE_CLUSTER = {"0.0.0.1", "0.0.0.2", "0.0.0.3"} local ONE_NODE_CLUSTER = {"127.0.0.1"} describe("Client", function() it("should be instanciable", function() assert.has_no_errors(function() - local client = Client({contact_points = {"127.0.0.1"}}) + local client = Client({contact_points = FAKE_CLUSTER}) assert.equal(false, client.connected) end) end) describe("#execute()", function() it("should return error if no host is available", function() - local client = Client({contact_points = {"0.0.0.1", "0.0.0.2", "0.0.0.3"}}) + local client = Client({contact_points = FAKE_CLUSTER}) local err = client:execute() assert.truthy(err) assert.equal("NoHostAvailableError", err.type) diff --git a/spec/rewrite_unit/requests_spec.lua b/spec/rewrite_unit/requests_spec.lua index dbbf1a9..e87387c 100644 --- a/spec/rewrite_unit/requests_spec.lua +++ b/spec/rewrite_unit/requests_spec.lua @@ -17,7 +17,7 @@ describe("Requests", function() buffer:write_integer(0) -- body length local req = Request({op_code = op_codes.STARTUP}) - assert.equal(buffer:write(), req:write()) + assert.equal(buffer:write(), req:get_full_frame()) end) it("should proxy all writer functions", function() local buffer = Buffer() @@ -33,17 +33,27 @@ describe("Requests", function() req:write_string_map({CQL_VERSION = "3.0.0"}) end) - assert.equal(buffer:write(), req:write()) + assert.equal(buffer:write(), req:get_full_frame()) end) end) describe("StartupRequest", function() it("should write a startup request", function() + -- Raw request local req = Request({op_code = op_codes.STARTUP}) req:write_string_map({CQL_VERSION = "3.0.0"}) + local full_buffer = Buffer(req:get_full_frame()) + -- Startup sugar request local startup = requests.StartupRequest() - assert.equal(req:write(), startup:write()) + assert.equal(0x03, full_buffer:read_byte()) + assert.equal(0, full_buffer:read_byte()) + assert.equal(0, full_buffer:read_byte()) + assert.equal(op_codes.STARTUP, full_buffer:read_byte()) + assert.equal(22, full_buffer:read_integer()) + assert.same({CQL_VERSION = "3.0.0"}, full_buffer:read_string_map()) + assert.equal(full_buffer:write(), req:get_full_frame()) + assert.equal(full_buffer:write(), startup:get_full_frame()) end) end) end) diff --git a/src/cassandra/control_connection.lua b/src/cassandra/control_connection.lua index 2782c6c..1e12e66 100644 --- a/src/cassandra/control_connection.lua +++ b/src/cassandra/control_connection.lua @@ -16,16 +16,15 @@ local SELECT_LOCAL_QUERY = "SELECT * FROM system.local WHERE key='local'" --- CONTROL_CONNECTION -- @section control_connection -local _CONTROL_CONNECTION = Object:extend() +local ControlConnection = Object:extend() -function _CONTROL_CONNECTION:new(options) +function ControlConnection:new(options) -- @TODO check attributes are valid (contact points, etc...) self.hosts = {} self.contact_points = options.contact_points - self.protocol_version = nil end -function _CONTROL_CONNECTION:init() +function ControlConnection:init() for _, contact_point in ipairs(self.contact_points) do -- Extract port if string is of the form "host:port" local addr, port = utils.split_by_colon(contact_point) @@ -45,8 +44,8 @@ function _CONTROL_CONNECTION:init() return self.hosts end -function _CONTROL_CONNECTION:get_peers() +function ControlConnection:get_peers() end -return _CONTROL_CONNECTION +return ControlConnection diff --git a/src/cassandra/errors.lua b/src/cassandra/errors.lua index 06f4748..30117ba 100644 --- a/src/cassandra/errors.lua +++ b/src/cassandra/errors.lua @@ -19,6 +19,12 @@ local ERROR_TYPES = { end return message end + }, + ResponseError = { + info = "Represents an error message from the server.", + message = function(code_translation, message) + return "["..code_translation.."] "..message + end } } @@ -46,14 +52,14 @@ end local _ERRORS = {} for k, v in pairs(ERROR_TYPES) do - _ERRORS[k] = function(message) + _ERRORS[k] = function(...) local err = { type = k, info = v.info, - message = type(v.message) == "function" and v.message(message) or message + message = type(v.message) == "function" and v.message(...) or arg[1] } - return setmetatable(err, error_mt) + return setmetatable(err, _error_mt) end end diff --git a/src/cassandra/frame_reader.lua b/src/cassandra/frame_reader.lua new file mode 100644 index 0000000..d402271 --- /dev/null +++ b/src/cassandra/frame_reader.lua @@ -0,0 +1,88 @@ +local Buffer = require "cassandra.buffer" +local errors = require "cassandra.errors" +local frame_header = require "cassandra.types.frame_header" +local op_codes = frame_header.op_codes +local bit = require "bit" + +--- CONST +-- @section constants + +local ERRORS = { + SERVER = 0x0000, + PROTOCOL = 0x000A, + BAD_CREDENTIALS = 0x0100, + UNAVAILABLE_EXCEPTION = 0x1000, + OVERLOADED = 0x1001, + IS_BOOTSTRAPPING = 0x1002, + TRUNCATE_ERROR = 0x1003, + WRITE_TIMEOUT = 0x1100, + READ_TIMEOUT = 0x1200, + SYNTAX_ERROR = 0x2000, + UNAUTHORIZED = 0x2100, + INVALID = 0x2200, + CONFIG_ERROR = 0x2300, + ALREADY_EXISTS = 0x2400, + UNPREPARED = 0x2500 +} + +local ERRORS_TRANSLATION = { + [ERRORS.SERVER] = "Server error", + [ERRORS.PROTOCOL] = "Protocol error", + [ERRORS.BAD_CREDENTIALS] = "Bad credentials", + [ERRORS.UNAVAILABLE_EXCEPTION] = "Unavailable exception", + [ERRORS.OVERLOADED] = "Overloaded", + [ERRORS.IS_BOOTSTRAPPING] = "Is bootstrapping", + [ERRORS.TRUNCATE_ERROR] = "Truncate error", + [ERRORS.WRITE_TIMEOUT] = "Write timeout", + [ERRORS.READ_TIMEOUT] = "Read timeout", + [ERRORS.SYNTAX_ERROR] = "Syntaxe rror", + [ERRORS.UNAUTHORIZED] = "Unauthorized", + [ERRORS.INVALID] = "Invalid", + [ERRORS.CONFIG_ERROR] = "Config error", + [ERRORS.ALREADY_EXISTS] = "Already exists", + [ERRORS.UNPREPARED] = "Unprepared" +} + +--- FrameHeader +-- @section frameheader + +local FrameReader = Buffer:extend() + +function FrameReader:new(frameHeader, raw_bytes) + self.frameHeader = frameHeader + + FrameReader.super.new(self, raw_bytes, frameHeader.version) +end + +local function read_frame(self) + +end + +local function parse_error(self) + local code = FrameReader.super.read_integer(self) + local message = FrameReader.super.read_string(self) + local code_translation = ERRORS_TRANSLATION[code] + return errors.ResponseError(code_translation, message) +end + +local function parse_ready(self) + return {ready = true} +end + +--- Decode a response frame +function FrameReader:read() + if self.frameHeader.op_code == nil then + error("frame header has no op_code") + end + + local op_code = self.frameHeader.op_code + + -- Parse frame depending on op_code + if op_code == op_codes.ERROR then + return nil, parse_error(self) + elseif op_code == op_codes.READY then + return parse_ready(self) + end +end + +return FrameReader diff --git a/src/cassandra/host.lua b/src/cassandra/host.lua index 28d4032..7533c2d 100644 --- a/src/cassandra/host.lua +++ b/src/cassandra/host.lua @@ -3,18 +3,18 @@ local Object = require "cassandra.classic" local HostConnection = require "cassandra.host_connection" local string_find = string.find ---- _HOST +--- Host -- @section host -local _HOST = Object:extend() +local Host = Object:extend() -function _HOST:new(host, port) - self.address = host..":"..port +function Host:new(address, port) + self.address = address..":"..port self.casandra_version = nil self.datacenter = nil self.rack = nil self.unhealthy_at = 0 - self.connection = HostConnection(host, port) + self.connection = HostConnection(address, port) end -return _HOST +return Host diff --git a/src/cassandra/host_connection.lua b/src/cassandra/host_connection.lua index e51d6a7..5ea1680 100644 --- a/src/cassandra/host_connection.lua +++ b/src/cassandra/host_connection.lua @@ -1,6 +1,11 @@ --- Represent one socket to connect to a Cassandra node local Object = require "cassandra.classic" local CONSTS = require "cassandra.consts" +local log = require "cassandra.log" +local requests = require "cassandra.requests" +local frame_header = require "cassandra.types.frame_header" +local FrameReader = require "cassandra.frame_reader" +local FrameHeader = frame_header.FrameHeader --- Constants -- @section constants @@ -28,32 +33,90 @@ local function new_socket() local socket, err = tcp_sock() if not socket then - return nil, err + return nil, nil, err end return socket, sock_type end ---- _HOST_CONNECTION +--- HostConnection -- @section host_connection -local _HOST_CONNECTION = Object:extend() +local HostConnection = Object:extend() -function _HOST_CONNECTION:new(host, port) - local socket, socket_type = new_socket() - self.host = host +function HostConnection:new(address, port) + local socket, socket_type, err = new_socket() + if err then + error(err) + end + self.address = address self.port = port self.socket = socket self.socket_type = socket_type + self.protocol_version = CONSTS.DEFAULT_PROTOCOL_VERSION end -local function startup() +--- Socket operations +-- @section socket + +local function send_and_receive(self, request) + request.version = self.protocol_version + + -- Send frame + local bytes_sent, err = self.socket:send(request:get_full_frame()) + if bytes_sent == nil then + return nil, err + end + local n_bytes_to_receive + if self.protocol_version < 3 then + n_bytes_to_receive = 8 + else + n_bytes_to_receive = 9 + end + + -- Receive frame header + local header_bytes, err = self.socket:receive(n_bytes_to_receive) + if header_bytes == nil then + return nil, err + end + local frameHeader = FrameHeader.from_raw_bytes(header_bytes) + + -- Receive frame body + local body_bytes, err = self.socket:receive(frameHeader.body_length) + if body_bytes == nil then + return nil, err + end + local frameReader = FrameReader(frameHeader, body_bytes) + + return frameReader:read() +end + +local function startup(self) + log.debug("Startup request. Trying to use protocol: "..self.protocol_version) + + local startup_req = requests.StartupRequest() + return send_and_receive(self, startup_req) end -function _HOST_CONNECTION:open() - local ok, err = self.socket:connect(self.host, self.port) - return ok == 1, err +function HostConnection:open() + local address = self.address..":"..self.port + log.debug("Connecting to "..address) + local ok, err = self.socket:connect(self.address, self.port) + if ok ~= 1 then + log.debug("Could not connect to "..address) + return false, err + end + log.debug("Socket connected to "..self.address) + + local res, err = startup(self) + if err then + log.debug("Startup request failed. "..err) + return false, err + elseif res.ready then + log.debug("Host at "..address.." is ready") + return true + end end -return _HOST_CONNECTION +return HostConnection diff --git a/src/cassandra/request_handler.lua b/src/cassandra/request_handler.lua index 24a6eab..d108534 100644 --- a/src/cassandra/request_handler.lua +++ b/src/cassandra/request_handler.lua @@ -1,12 +1,13 @@ local Object = require "cassandra.classic" local Errors = require "cassandra.errors" +local ipairs = ipairs ---- _REQUEST_HANDLER +--- RequestHandler -- @section request_handler -local _REQUEST_HANDLER = Object:extend() +local RequestHandler = Object:extend() -function _REQUEST_HANDLER:mew(options) +function RequestHandler:mew(options) self.loadBalancingPolicy = nil -- @TODO self.retryPolicy = nil -- @TODO self.request = options.request @@ -14,7 +15,7 @@ function _REQUEST_HANDLER:mew(options) end -- Get the first connection from the available one with no regards for the load balancing policy -function _REQUEST_HANDLER.get_first_host(hosts) +function RequestHandler.get_first_host(hosts) local errors = {} for _, host in ipairs(hosts) do local connected, err = host.connection:open() @@ -28,4 +29,4 @@ function _REQUEST_HANDLER.get_first_host(hosts) return nil, Errors.NoHostAvailableError(errors) end -return _REQUEST_HANDLER +return RequestHandler diff --git a/src/cassandra/requests.lua b/src/cassandra/requests.lua index 9238773..8a123e5 100644 --- a/src/cassandra/requests.lua +++ b/src/cassandra/requests.lua @@ -13,13 +13,13 @@ local Request = Buffer:extend() function Request:new(options) if options == nil then options = {} end - self.version = options.version and options.version or CONSTS.DEFAULT_PROTOCOL_VERSION + self.version = nil -- to be set by host_connection.lua before being sent self.op_code = options.op_code Request.super.new(self, nil, self.version) end -function Request:write(flags) +function Request:get_full_frame(flags) if not self.op_code then error("Request#write() has no op_code") end local frameHeader = FrameHeader(self.version, flags, self.op_code, self.len) @@ -31,10 +31,8 @@ end local StartupRequest = Request:extend() -function StartupRequest:new(...) - StartupRequest.super.new(self, ...) - - self.op_code = op_codes.STARTUP +function StartupRequest:new() + StartupRequest.super.new(self, {op_code = op_codes.STARTUP}) StartupRequest.super.write_string_map(self, { CQL_VERSION = CONSTS.CQL_VERSION }) diff --git a/src/cassandra/types/frame_header.lua b/src/cassandra/types/frame_header.lua index f34a529..f3b078e 100644 --- a/src/cassandra/types/frame_header.lua +++ b/src/cassandra/types/frame_header.lua @@ -1,4 +1,5 @@ local utils = require "cassandra.utils" +local bit = require "bit" local Buffer = require "cassandra.buffer" --- CONST @@ -22,7 +23,9 @@ local FLAGS = { TRACING = 0x02 } -setmetatable(FLAGS, utils.const_mt) +-- when we'll support protocol v4, other +-- flags will be added. +-- setmetatable(FLAGS, utils.const_mt) local OP_CODES = { ERROR = 0x00, @@ -43,8 +46,6 @@ local OP_CODES = { AUTH_SUCCESS = 0x10 } -setmetatable(OP_CODES, utils.const_mt) - --- FrameHeader -- @section FrameHeader @@ -53,21 +54,39 @@ local FrameHeader = Buffer:extend() function FrameHeader:new(version, flags, op_code, body_length) self.flags = flags and flags or 0 self.op_code = op_code + self.stream_id = 0 -- @TODO support streaming self.body_length = body_length self.super.new(self, nil, version) end function FrameHeader:write() - self.super.write_byte(self, VERSION_CODES[self.version].REQUEST) - self.super.write_byte(self, self.flags) -- @TODO make sure to expose flags to the client or find a more secure way - self.super.write_byte(self, 0) -- @TODO support streaming - self.super.write_byte(self, self.op_code) -- @TODO make sure to expose op_codes to the client or find a more secure way + self.super.write_byte(self, VERSION_CODES:get("REQUEST", self.version)) + self.super.write_byte(self, self.flags) -- @TODO find a more secure way + self.super.write_byte(self, self.stream_id) + self.super.write_byte(self, self.op_code) -- @TODO find a more secure way self.super.write_integer(self, self.body_length) return self.super.write(self) end +function FrameHeader.from_raw_bytes(raw_bytes) + local buffer = Buffer(raw_bytes) + local version = bit.band(buffer:read_byte(), 0x7F) + buffer.version = version + local flags = buffer:read_byte() + local stream_id + if version < 3 then + stream_id = buffer:read_byte() + else + stream_id = buffer:read_short() + end + local op_code = buffer:read_byte() + local body_length = buffer:read_integer() + + return FrameHeader(version, flags, op_code, body_length) +end + return { op_codes = OP_CODES, flags = FLAGS, diff --git a/src/cassandra/utils/buffer.lua b/src/cassandra/utils/buffer.lua index 9edd947..9f705d7 100644 --- a/src/cassandra/utils/buffer.lua +++ b/src/cassandra/utils/buffer.lua @@ -16,16 +16,17 @@ function Buffer:write() return self.str end -function Buffer:write_bytes(value) - self.str = self.str..value - self.len = self.len + #value +function Buffer:write_bytes(bytes) + self.str = self.str..bytes + self.len = self.len + #bytes self.pos = self.len end function Buffer:read_bytes(n_bytes_to_read) local last_index = n_bytes_to_read ~= nil and self.pos + n_bytes_to_read - 1 or -1 local bytes = string_sub(self.str, self.pos, last_index) - self.pos = self.pos + #bytes return bytes + self.pos = self.pos + #bytes + return bytes end function Buffer.from_buffer(buffer) From 29b6bc63ed3737ed7596d1ff1a0c36e6e1927a15 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Mon, 2 Nov 2015 14:22:25 -0800 Subject: [PATCH 05/78] fix(rewrite) use short for streamId in protocol 3+ --- spec/rewrite_unit/requests_spec.lua | 38 +++++++++++++++++++++------- src/cassandra/host_connection.lua | 8 +++--- src/cassandra/requests.lua | 8 +++--- src/cassandra/types/frame_header.lua | 10 +++++++- 4 files changed, 48 insertions(+), 16 deletions(-) diff --git a/spec/rewrite_unit/requests_spec.lua b/spec/rewrite_unit/requests_spec.lua index e87387c..e777edf 100644 --- a/spec/rewrite_unit/requests_spec.lua +++ b/spec/rewrite_unit/requests_spec.lua @@ -9,26 +9,26 @@ describe("Requests", function() local Request = requests.Request describe("Request", function() it("should write its own frame", function() - local buffer = Buffer() + local buffer = Buffer(nil, 3) buffer:write_byte(0x03) buffer:write_byte(0) -- flags - buffer:write_byte(0) -- stream id + buffer:write_short(0) -- stream id buffer:write_byte(op_codes.STARTUP) buffer:write_integer(0) -- body length - local req = Request({op_code = op_codes.STARTUP}) + local req = Request({version = 3, op_code = op_codes.STARTUP}) assert.equal(buffer:write(), req:get_full_frame()) end) it("should proxy all writer functions", function() - local buffer = Buffer() + local buffer = Buffer(nil, 3) buffer:write_byte(0x03) buffer:write_byte(0) -- flags - buffer:write_byte(0) -- stream id + buffer:write_short(0) -- stream id buffer:write_byte(op_codes.STARTUP) buffer:write_integer(22) -- body length buffer:write_string_map({CQL_VERSION = "3.0.0"}) - local req = Request({op_code = op_codes.STARTUP}) + local req = Request({version = 3, op_code = op_codes.STARTUP}) assert.has_no_errors(function() req:write_string_map({CQL_VERSION = "3.0.0"}) end) @@ -39,15 +39,35 @@ describe("Requests", function() describe("StartupRequest", function() it("should write a startup request", function() -- Raw request - local req = Request({op_code = op_codes.STARTUP}) + local req = Request({version = 3, op_code = op_codes.STARTUP}) req:write_string_map({CQL_VERSION = "3.0.0"}) - local full_buffer = Buffer(req:get_full_frame()) + local full_buffer = Buffer(req:get_full_frame(), 3) -- Startup sugar request - local startup = requests.StartupRequest() + local startup = requests.StartupRequest({version = 3}) assert.equal(0x03, full_buffer:read_byte()) assert.equal(0, full_buffer:read_byte()) + assert.equal(0, full_buffer:read_short()) + assert.equal(op_codes.STARTUP, full_buffer:read_byte()) + assert.equal(22, full_buffer:read_integer()) + assert.same({CQL_VERSION = "3.0.0"}, full_buffer:read_string_map()) + assert.equal(full_buffer:write(), req:get_full_frame()) + assert.equal(full_buffer:write(), startup:get_full_frame()) + end) + end) + describe("Protocol versions", function() + it("should support other versions of the protocol", function() + -- Raw request + local req = Request({version = 2, op_code = op_codes.STARTUP}) + req:write_string_map({CQL_VERSION = "3.0.0"}) + local full_buffer = Buffer(req:get_full_frame(), 2) + + -- Startup sugar request + local startup = requests.StartupRequest({version = 2}) + + assert.equal(0x02, full_buffer:read_byte()) + assert.equal(0, full_buffer:read_byte()) assert.equal(0, full_buffer:read_byte()) assert.equal(op_codes.STARTUP, full_buffer:read_byte()) assert.equal(22, full_buffer:read_integer()) diff --git a/src/cassandra/host_connection.lua b/src/cassandra/host_connection.lua index 5ea1680..2770377 100644 --- a/src/cassandra/host_connection.lua +++ b/src/cassandra/host_connection.lua @@ -83,9 +83,11 @@ local function send_and_receive(self, request) local frameHeader = FrameHeader.from_raw_bytes(header_bytes) -- Receive frame body - local body_bytes, err = self.socket:receive(frameHeader.body_length) - if body_bytes == nil then - return nil, err + if frameHeader.body_length > 0 then + local body_bytes, err = self.socket:receive(frameHeader.body_length) + if body_bytes == nil then + return nil, err + end end local frameReader = FrameReader(frameHeader, body_bytes) diff --git a/src/cassandra/requests.lua b/src/cassandra/requests.lua index 8a123e5..2c596d7 100644 --- a/src/cassandra/requests.lua +++ b/src/cassandra/requests.lua @@ -13,7 +13,7 @@ local Request = Buffer:extend() function Request:new(options) if options == nil then options = {} end - self.version = nil -- to be set by host_connection.lua before being sent + self.version = options.version -- to be set by host_connection.lua before being sent self.op_code = options.op_code Request.super.new(self, nil, self.version) @@ -31,8 +31,10 @@ end local StartupRequest = Request:extend() -function StartupRequest:new() - StartupRequest.super.new(self, {op_code = op_codes.STARTUP}) +function StartupRequest:new(options) + if options == nil then options = {} end + options.op_code = op_codes.STARTUP + StartupRequest.super.new(self, options) StartupRequest.super.write_string_map(self, { CQL_VERSION = CONSTS.CQL_VERSION }) diff --git a/src/cassandra/types/frame_header.lua b/src/cassandra/types/frame_header.lua index f3b078e..a97549e 100644 --- a/src/cassandra/types/frame_header.lua +++ b/src/cassandra/types/frame_header.lua @@ -63,7 +63,13 @@ end function FrameHeader:write() self.super.write_byte(self, VERSION_CODES:get("REQUEST", self.version)) self.super.write_byte(self, self.flags) -- @TODO find a more secure way - self.super.write_byte(self, self.stream_id) + + if self.version < 3 then + self.super.write_byte(self, self.stream_id) + else + self.super.write_short(self, self.stream_id) + end + self.super.write_byte(self, self.op_code) -- @TODO find a more secure way self.super.write_integer(self, self.body_length) @@ -75,12 +81,14 @@ function FrameHeader.from_raw_bytes(raw_bytes) local version = bit.band(buffer:read_byte(), 0x7F) buffer.version = version local flags = buffer:read_byte() + local stream_id if version < 3 then stream_id = buffer:read_byte() else stream_id = buffer:read_short() end + local op_code = buffer:read_byte() local body_length = buffer:read_integer() From d2bfe9e1e8f54d73e03f14470c1abfdd0953810d Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Mon, 2 Nov 2015 18:14:30 -0800 Subject: [PATCH 06/78] feat(rewrite) support protocol decreasing --- spec/integration_spec.lua | 6 ++ spec/rewrite_integration/client_spec.lua | 17 +++++- src/cassandra/client.lua | 2 - src/cassandra/control_connection.lua | 1 + src/cassandra/errors.lua | 15 ++++- src/cassandra/frame_reader.lua | 7 ++- src/cassandra/host_connection.lua | 76 +++++++++++++++++------- src/cassandra/log.lua | 13 ++-- src/cassandra/protocol/reader_v3.lua | 6 +- src/cassandra/types/frame_header.lua | 17 +++++- 10 files changed, 121 insertions(+), 39 deletions(-) diff --git a/spec/integration_spec.lua b/spec/integration_spec.lua index 471faf0..e95c104 100644 --- a/spec/integration_spec.lua +++ b/spec/integration_spec.lua @@ -1,6 +1,12 @@ local cassandra_v2 = require "cassandra.v2" local cassandra_v3 = require "cassandra" +local sess = cassandra_v3:new() +local ok, err = sess:connect({"127.0.0.1:9041"}) +local inspect = require "inspect" +print(inspect(err)) + + describe("Session", function() for _, cass in ipairs({{v = "v2", c = cassandra_v2}, { v = "v3", c = cassandra_v3}}) do local cassandra = cass.c diff --git a/spec/rewrite_integration/client_spec.lua b/spec/rewrite_integration/client_spec.lua index cc23f02..8a583c8 100644 --- a/spec/rewrite_integration/client_spec.lua +++ b/spec/rewrite_integration/client_spec.lua @@ -2,6 +2,7 @@ local Client = require "cassandra.client" local FAKE_CLUSTER = {"0.0.0.1", "0.0.0.2", "0.0.0.3"} local ONE_NODE_CLUSTER = {"127.0.0.1"} +local ONE_NODE_CLUSTER_2_0 = {"127.0.0.1:9041"} describe("Client", function() it("should be instanciable", function() @@ -17,9 +18,23 @@ describe("Client", function() assert.truthy(err) assert.equal("NoHostAvailableError", err.type) end) - it("TODO", function() + it("should connect to a cluster", function() local client = Client({contact_points = ONE_NODE_CLUSTER}) local err = client:execute() + assert.falsy(err) + end) + it("should retrieve cluter information when connecting", function() + local client = Client({contact_points = ONE_NODE_CLUSTER}) + local err = client:execute() + assert.falsy(err) + end) + end) + describe("binary protocol downgrade", function() + it("should downgrade the protocol version if the node does not support the most recent one", function() + pending() + local client = Client({contact_points = ONE_NODE_CLUSTER_2_0}) + local err = client:execute() + assert.falsy(err) end) end) end) diff --git a/src/cassandra/client.lua b/src/cassandra/client.lua index da42d53..447f321 100644 --- a/src/cassandra/client.lua +++ b/src/cassandra/client.lua @@ -21,8 +21,6 @@ local function _connect(self) local err self.hosts, err = self.controlConnection:init() - local inspect = require "inspect" - print("Hosts: ", inspect(self.hosts)) if err then return err end diff --git a/src/cassandra/control_connection.lua b/src/cassandra/control_connection.lua index 1e12e66..fc3a257 100644 --- a/src/cassandra/control_connection.lua +++ b/src/cassandra/control_connection.lua @@ -36,6 +36,7 @@ function ControlConnection:init() if err then return nil, err end + -- @TODO get peers info -- @TODO get local info -- local peers, err diff --git a/src/cassandra/errors.lua b/src/cassandra/errors.lua index 30117ba..c1af08e 100644 --- a/src/cassandra/errors.lua +++ b/src/cassandra/errors.lua @@ -22,8 +22,11 @@ local ERROR_TYPES = { }, ResponseError = { info = "Represents an error message from the server.", - message = function(code_translation, message) + message = function(code, code_translation, message) return "["..code_translation.."] "..message + end, + meta = function(code) + return {code = code} end } } @@ -53,12 +56,22 @@ local _ERRORS = {} for k, v in pairs(ERROR_TYPES) do _ERRORS[k] = function(...) + local arg = {...} local err = { type = k, info = v.info, message = type(v.message) == "function" and v.message(...) or arg[1] } + if type(v.meta) == "function" then + local meta = v.meta(...) + for meta_k, meta_v in pairs(meta) do + if err[meta_k] == nil then + err[meta_k] = meta_v + end + end + end + return setmetatable(err, _error_mt) end end diff --git a/src/cassandra/frame_reader.lua b/src/cassandra/frame_reader.lua index d402271..f37ffa3 100644 --- a/src/cassandra/frame_reader.lua +++ b/src/cassandra/frame_reader.lua @@ -62,7 +62,7 @@ local function parse_error(self) local code = FrameReader.super.read_integer(self) local message = FrameReader.super.read_string(self) local code_translation = ERRORS_TRANSLATION[code] - return errors.ResponseError(code_translation, message) + return errors.ResponseError(code, code_translation, message) end local function parse_ready(self) @@ -85,4 +85,7 @@ function FrameReader:read() end end -return FrameReader +return { + FrameReader = FrameReader, + errors = ERRORS +} diff --git a/src/cassandra/host_connection.lua b/src/cassandra/host_connection.lua index 2770377..1522021 100644 --- a/src/cassandra/host_connection.lua +++ b/src/cassandra/host_connection.lua @@ -4,7 +4,10 @@ local CONSTS = require "cassandra.consts" local log = require "cassandra.log" local requests = require "cassandra.requests" local frame_header = require "cassandra.types.frame_header" -local FrameReader = require "cassandra.frame_reader" +local frame_reader = require "cassandra.frame_reader" +local string_find = string.find + +local FrameReader = frame_reader.FrameReader local FrameHeader = frame_header.FrameHeader --- Constants @@ -18,7 +21,7 @@ local SOCKET_TYPES = { --- Utils -- @section utils -local function new_socket() +local function new_socket(self) local tcp_sock, sock_type if ngx and ngx.get_phase ~= nil and ngx.get_phase ~= "init" then @@ -33,10 +36,11 @@ local function new_socket() local socket, err = tcp_sock() if not socket then - return nil, nil, err + error(err) end - return socket, sock_type + self.socket = socket + self.socket_type = sock_type end --- HostConnection @@ -45,14 +49,8 @@ end local HostConnection = Object:extend() function HostConnection:new(address, port) - local socket, socket_type, err = new_socket() - if err then - error(err) - end self.address = address self.port = port - self.socket = socket - self.socket_type = socket_type self.protocol_version = CONSTS.DEFAULT_PROTOCOL_VERSION end @@ -68,55 +66,87 @@ local function send_and_receive(self, request) return nil, err end - local n_bytes_to_receive - if self.protocol_version < 3 then - n_bytes_to_receive = 8 - else - n_bytes_to_receive = 9 + -- Receive frame version + local frame_version_byte, err = self.socket:receive(1) + if frame_version_byte == nil then + return nil, err end + local n_bytes_to_receive = FrameHeader.size_from_byte(frame_version_byte) - 1 + -- Receive frame header local header_bytes, err = self.socket:receive(n_bytes_to_receive) if header_bytes == nil then return nil, err end - local frameHeader = FrameHeader.from_raw_bytes(header_bytes) + + local frameHeader = FrameHeader.from_raw_bytes(frame_version_byte, header_bytes) -- Receive frame body + local body_bytes if frameHeader.body_length > 0 then - local body_bytes, err = self.socket:receive(frameHeader.body_length) + body_bytes, err = self.socket:receive(frameHeader.body_length) if body_bytes == nil then return nil, err end end + local frameReader = FrameReader(frameHeader, body_bytes) return frameReader:read() end +--- Determine the protocol version to use and send the STARTUP request local function startup(self) - log.debug("Startup request. Trying to use protocol: "..self.protocol_version) + log.debug("Startup request. Trying to use protocol v"..self.protocol_version) local startup_req = requests.StartupRequest() return send_and_receive(self, startup_req) end +function HostConnection:decrease_version() + self.protocol_version = self.protocol_version - 1 + if self.protocol_version < CONSTS.MIN_PROTOCOL_VERSION then + error("minimum protocol version supported: ", CONSTS.MIN_PROTOCOL_VERSION) + end +end + +function HostConnection:close() + local res, err = self.socket:close() + if err then + log.err("Could not close socket for connection to "..self.address..":"..self.port..". ", err) + end + return res == 1 +end + function HostConnection:open() local address = self.address..":"..self.port - log.debug("Connecting to "..address) + new_socket(self) + + log.debug("Connecting to ", address) local ok, err = self.socket:connect(self.address, self.port) if ok ~= 1 then - log.debug("Could not connect to "..address) + log.debug("Could not connect to "..address, err) return false, err end - log.debug("Socket connected to "..self.address) + log.debug("Socket connected to ", address) local res, err = startup(self) if err then - log.debug("Startup request failed. "..err) + log.debug("Startup request failed. ", err) + -- Check for incorrect protocol version + if err and err.code == frame_reader.errors.PROTOCOL then + if string_find(err.message, "Invalid or unsupported protocol version:", nil, true) then + self:close() + self:decrease_version() + log.debug("Decreasing protocol version to v"..self.protocol_version) + return self:open() + end + end + return false, err elseif res.ready then - log.debug("Host at "..address.." is ready") + log.debug("Host at "..address.." is ready with protocol v"..self.protocol_version) return true end end diff --git a/src/cassandra/log.lua b/src/cassandra/log.lua index 89d0ec0..c06b76d 100644 --- a/src/cassandra/log.lua +++ b/src/cassandra/log.lua @@ -1,4 +1,6 @@ local string_format = string.format +local unpack = unpack +local type = type local LEVELS = { "ERR", @@ -8,18 +10,19 @@ local LEVELS = { local _LOG = {} -local function log(level, message) +local function log(level, ...) + local arg = {...} if ngx and type(ngx.log) == "function" then -- lua-nginx-module - ngx.log(ngx[level], message) + ngx.log(ngx[level], unpack(arg)) else - print(string_format("%s: %s", level, message)) -- can't configure level for now + print(string_format("%s: ", level), unpack(arg)) -- can't configure level for now end end for _, level in ipairs(LEVELS) do - _LOG[level:lower()] = function(message) - log(level, message) + _LOG[level:lower()] = function(...) + log(level, ...) end end diff --git a/src/cassandra/protocol/reader_v3.lua b/src/cassandra/protocol/reader_v3.lua index f0f2f09..b21f114 100644 --- a/src/cassandra/protocol/reader_v3.lua +++ b/src/cassandra/protocol/reader_v3.lua @@ -5,17 +5,17 @@ local _M = Reader_v2:extend() function _M:receive_frame(session) local unmarshaller = self.unmarshaller - local header, err = session.socket:receive(9) + local header, err = session.socket:receive(8) if not header then return nil, string.format("Failed to read frame header from %s: %s", session.host, err) end local header_buffer = unmarshaller:create_buffer(header) local version = unmarshaller:read_raw_byte(header_buffer) if version ~= self.constants.version_codes.RESPONSE then - return nil, string.format("Invalid response version received from %s", session.host) + --return nil, string.format("Invalid response version received from %s", session.host) end local flags = unmarshaller:read_raw_byte(header_buffer) - local stream = unmarshaller:read_short(header_buffer) + local stream = unmarshaller:read_raw_byte(header_buffer) local op_code = unmarshaller:read_raw_byte(header_buffer) local length = unmarshaller:read_int(header_buffer) diff --git a/src/cassandra/types/frame_header.lua b/src/cassandra/types/frame_header.lua index a97549e..e44c771 100644 --- a/src/cassandra/types/frame_header.lua +++ b/src/cassandra/types/frame_header.lua @@ -76,9 +76,22 @@ function FrameHeader:write() return self.super.write(self) end -function FrameHeader.from_raw_bytes(raw_bytes) +function FrameHeader.version_from_byte(byte) + local buf = Buffer(byte) + return bit.band(buf:read_byte(), 0x7F) +end + +function FrameHeader.size_from_byte(version_byte) + if FrameHeader.version_from_byte(version_byte) < 3 then + return 8 + else + return 9 + end +end + +function FrameHeader.from_raw_bytes(version_byte, raw_bytes) local buffer = Buffer(raw_bytes) - local version = bit.band(buffer:read_byte(), 0x7F) + local version = FrameHeader.version_from_byte(version_byte) buffer.version = version local flags = buffer:read_byte() From 022b6e3030ef63a2afac1afe61a292a41edfde4e Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Tue, 3 Nov 2015 20:20:43 -0800 Subject: [PATCH 07/78] refactor(rewrite) CQL types buffering --- spec/rewrite_integration/client_spec.lua | 13 +- spec/rewrite_unit/buffer_spec.lua | 47 +- spec/rewrite_unit/cql_types_buffer_spec.lua | 36 ++ spec/rewrite_unit/requests_spec.lua | 56 +- src/cassandra/buffer.lua | 114 +++- src/cassandra/client.lua | 8 +- src/cassandra/client_options.lua | 2 +- src/cassandra/consts.lua | 3 +- src/cassandra/control_connection.lua | 20 +- src/cassandra/frame_reader.lua | 118 ++++- src/cassandra/host_connection.lua | 38 +- src/cassandra/requests.lua | 77 ++- src/cassandra/types/boolean.lua | 11 +- src/cassandra/types/byte.lua | 7 +- src/cassandra/types/bytes.lua | 11 + src/cassandra/types/cql_types.lua | 24 + src/cassandra/types/frame_header.lua | 29 +- src/cassandra/types/inet.lua | 57 ++ src/cassandra/types/int.lua | 11 + src/cassandra/types/integer.lua | 11 - src/cassandra/types/long_string.lua | 9 + src/cassandra/types/options.lua | 14 + src/cassandra/types/raw.lua | 8 + src/cassandra/types/set.lua | 27 + src/cassandra/types/short.lua | 8 +- src/cassandra/types/string.lua | 11 +- src/cassandra/types/string_map.lua | 22 +- src/cassandra/utils/bit.lua | 545 ++++++++++++++++++++ src/cassandra/utils/buffer.lua | 19 +- src/cassandra/utils/table.lua | 20 + 30 files changed, 1191 insertions(+), 185 deletions(-) create mode 100644 spec/rewrite_unit/cql_types_buffer_spec.lua create mode 100644 src/cassandra/types/bytes.lua create mode 100644 src/cassandra/types/cql_types.lua create mode 100644 src/cassandra/types/inet.lua create mode 100644 src/cassandra/types/int.lua delete mode 100644 src/cassandra/types/integer.lua create mode 100644 src/cassandra/types/long_string.lua create mode 100644 src/cassandra/types/options.lua create mode 100644 src/cassandra/types/raw.lua create mode 100644 src/cassandra/types/set.lua create mode 100644 src/cassandra/utils/bit.lua create mode 100644 src/cassandra/utils/table.lua diff --git a/spec/rewrite_integration/client_spec.lua b/spec/rewrite_integration/client_spec.lua index 8a583c8..ea4db02 100644 --- a/spec/rewrite_integration/client_spec.lua +++ b/spec/rewrite_integration/client_spec.lua @@ -1,8 +1,8 @@ local Client = require "cassandra.client" local FAKE_CLUSTER = {"0.0.0.1", "0.0.0.2", "0.0.0.3"} -local ONE_NODE_CLUSTER = {"127.0.0.1"} -local ONE_NODE_CLUSTER_2_0 = {"127.0.0.1:9041"} +local CLUSTER_2_0 = {"127.0.0.1:9001", "127.0.0.1:9002", "127.0.0.1:9003"} +local CLUSTER_2_1 = {"127.0.0.1:9101", "127.0.0.1:9102", "127.0.0.1:9103"} describe("Client", function() it("should be instanciable", function() @@ -19,12 +19,13 @@ describe("Client", function() assert.equal("NoHostAvailableError", err.type) end) it("should connect to a cluster", function() - local client = Client({contact_points = ONE_NODE_CLUSTER}) + pending() + local client = Client({contact_points = CLUSTER_2_1}) local err = client:execute() assert.falsy(err) end) - it("should retrieve cluter information when connecting", function() - local client = Client({contact_points = ONE_NODE_CLUSTER}) + it("should retrieve cluster information when connecting", function() + local client = Client({contact_points = CLUSTER_2_1}) local err = client:execute() assert.falsy(err) end) @@ -32,7 +33,7 @@ describe("Client", function() describe("binary protocol downgrade", function() it("should downgrade the protocol version if the node does not support the most recent one", function() pending() - local client = Client({contact_points = ONE_NODE_CLUSTER_2_0}) + local client = Client({contact_points = CLUSTER_2_0}) local err = client:execute() assert.falsy(err) end) diff --git a/spec/rewrite_unit/buffer_spec.lua b/spec/rewrite_unit/buffer_spec.lua index 3629167..f254b95 100644 --- a/spec/rewrite_unit/buffer_spec.lua +++ b/spec/rewrite_unit/buffer_spec.lua @@ -2,25 +2,31 @@ local Buffer = require "cassandra.buffer" describe("Buffer", function() local FIXTURES = { - short = {0, 1, -1, 12, 13}, byte = {1, 2, 3}, - boolean = {true, false}, - integer = {0, 4200, -42}, + int = {0, 4200, -42}, + short = {0, 1, -1, 12, 13}, + --boolean = {true, false}, string = {"hello world"}, + long_string = {string.rep("blob", 1000), ""}, + inet = { + "127.0.0.1", "0.0.0.1", "8.8.8.8", + "2001:0db8:85a3:0042:1000:8a2e:0370:7334", + "2001:0db8:0000:0000:0000:0000:0000:0001" + }, string_map = { {hello = "world"}, {cql_version = "3.0.0", foo = "bar"} - } + }, } for fixture_type, fixture_values in pairs(FIXTURES) do it("["..fixture_type.."] should be bufferable", function() for _, fixture in ipairs(fixture_values) do - local writer = Buffer() + local writer = Buffer(3) writer["write_"..fixture_type](writer, fixture) - local bytes = writer:write() + local bytes = writer:dump() - local reader = Buffer(bytes) + local reader = Buffer(3, bytes) -- protocol v3 local decoded = reader["read_"..fixture_type](reader) if type(fixture) == "table" then @@ -33,14 +39,35 @@ describe("Buffer", function() end it("should accumulate values", function() - local writer = Buffer() + local writer = Buffer(3) -- protocol v3 writer:write_byte(2) - writer:write_integer(128) + writer:write_int(128) writer:write_string("hello world") local reader = Buffer.from_buffer(writer) assert.equal(2, reader:read_byte()) - assert.equal(128, reader:read_integer()) + assert.equal(128, reader:read_int()) assert.equal("hello world", reader:read_string()) end) + + describe("inet", function() + local fixtures = { + ["2001:0db8:85a3:0042:1000:8a2e:0370:7334"] = "2001:0db8:85a3:0042:1000:8a2e:0370:7334", + ["2001:0db8:0000:0000:0000:0000:0000:0001"] = "2001:db8::1", + ["2001:0db8:85a3:0000:0000:0000:0000:0010"] = "2001:db8:85a3::10", + ["2001:0db8:85a3:0000:0000:0000:0000:0100"] = "2001:db8:85a3::100", + ["0000:0000:0000:0000:0000:0000:0000:0001"] = "::1", + ["0000:0000:0000:0000:0000:0000:0000:0000"] = "::" + } + + it("should shorten ipv6 addresses", function() + for expected_ip, fixture_ip in pairs(fixtures) do + local buffer = Buffer(3) + buffer:write_inet(fixture_ip) + buffer.pos = 1 + + assert.equal(expected_ip, buffer:read_inet()) + end + end) + end) end) diff --git a/spec/rewrite_unit/cql_types_buffer_spec.lua b/spec/rewrite_unit/cql_types_buffer_spec.lua new file mode 100644 index 0000000..870c1ff --- /dev/null +++ b/spec/rewrite_unit/cql_types_buffer_spec.lua @@ -0,0 +1,36 @@ +local Buffer = require "cassandra.buffer" + +describe("CQL Types", function() + local FIXTURES = { + boolean = {true, false}, + inet = { + "127.0.0.1", "0.0.0.1", "8.8.8.8", + "2001:0db8:85a3:0042:1000:8a2e:0370:7334", + "2001:0db8:0000:0000:0000:0000:0000:0001" + }, + int = {0, 4200, -42}, + set = { + {"abc", "def"}, + {0, 1, 2, 42, -42} + }, + } + + for fixture_type, fixture_values in pairs(FIXTURES) do + it("["..fixture_type.."] should be bufferable", function() + for _, fixture in ipairs(fixture_values) do + local writer = Buffer(3) + writer["write_cql_"..fixture_type](writer, fixture) + local bytes = writer:dump() + + local reader = Buffer(3, bytes) -- protocol v3 + local decoded = reader["read_cql_"..fixture_type](reader) + + if type(fixture) == "table" then + assert.same(fixture, decoded) + else + assert.equal(fixture, decoded) + end + end + end) + end +end) diff --git a/spec/rewrite_unit/requests_spec.lua b/spec/rewrite_unit/requests_spec.lua index e777edf..f83fd58 100644 --- a/spec/rewrite_unit/requests_spec.lua +++ b/spec/rewrite_unit/requests_spec.lua @@ -6,74 +6,34 @@ local frame_header = require "cassandra.types.frame_header" local op_codes = frame_header.op_codes describe("Requests", function() - local Request = requests.Request - describe("Request", function() - it("should write its own frame", function() - local buffer = Buffer(nil, 3) - buffer:write_byte(0x03) - buffer:write_byte(0) -- flags - buffer:write_short(0) -- stream id - buffer:write_byte(op_codes.STARTUP) - buffer:write_integer(0) -- body length - - local req = Request({version = 3, op_code = op_codes.STARTUP}) - assert.equal(buffer:write(), req:get_full_frame()) - end) - it("should proxy all writer functions", function() - local buffer = Buffer(nil, 3) - buffer:write_byte(0x03) - buffer:write_byte(0) -- flags - buffer:write_short(0) -- stream id - buffer:write_byte(op_codes.STARTUP) - buffer:write_integer(22) -- body length - buffer:write_string_map({CQL_VERSION = "3.0.0"}) - - local req = Request({version = 3, op_code = op_codes.STARTUP}) - assert.has_no_errors(function() - req:write_string_map({CQL_VERSION = "3.0.0"}) - end) - - assert.equal(buffer:write(), req:get_full_frame()) - end) - end) describe("StartupRequest", function() it("should write a startup request", function() - -- Raw request - local req = Request({version = 3, op_code = op_codes.STARTUP}) - req:write_string_map({CQL_VERSION = "3.0.0"}) - local full_buffer = Buffer(req:get_full_frame(), 3) + local startup = requests.StartupRequest() + startup:set_version(3) - -- Startup sugar request - local startup = requests.StartupRequest({version = 3}) + local full_buffer = Buffer(3, startup:get_full_frame()) assert.equal(0x03, full_buffer:read_byte()) assert.equal(0, full_buffer:read_byte()) assert.equal(0, full_buffer:read_short()) assert.equal(op_codes.STARTUP, full_buffer:read_byte()) - assert.equal(22, full_buffer:read_integer()) + assert.equal(22, full_buffer:read_int()) assert.same({CQL_VERSION = "3.0.0"}, full_buffer:read_string_map()) - assert.equal(full_buffer:write(), req:get_full_frame()) - assert.equal(full_buffer:write(), startup:get_full_frame()) end) end) describe("Protocol versions", function() it("should support other versions of the protocol", function() - -- Raw request - local req = Request({version = 2, op_code = op_codes.STARTUP}) - req:write_string_map({CQL_VERSION = "3.0.0"}) - local full_buffer = Buffer(req:get_full_frame(), 2) + local startup = requests.StartupRequest() + startup:set_version(2) - -- Startup sugar request - local startup = requests.StartupRequest({version = 2}) + local full_buffer = Buffer(3, startup:get_full_frame()) assert.equal(0x02, full_buffer:read_byte()) assert.equal(0, full_buffer:read_byte()) assert.equal(0, full_buffer:read_byte()) assert.equal(op_codes.STARTUP, full_buffer:read_byte()) - assert.equal(22, full_buffer:read_integer()) + assert.equal(22, full_buffer:read_int()) assert.same({CQL_VERSION = "3.0.0"}, full_buffer:read_string_map()) - assert.equal(full_buffer:write(), req:get_full_frame()) - assert.equal(full_buffer:write(), startup:get_full_frame()) end) end) end) diff --git a/src/cassandra/buffer.lua b/src/cassandra/buffer.lua index 7fba0c1..1d3a306 100644 --- a/src/cassandra/buffer.lua +++ b/src/cassandra/buffer.lua @@ -1,18 +1,124 @@ local Buffer = require "cassandra.utils.buffer" +local CQL_TYPES = require "cassandra.types.cql_types" +local math_floor = math.floor + +--- Frame types +-- @section frame_types local TYPES = { "byte", + "int", + -- "long", "short", - "boolean", - "integer", "string", - "string_map" + "long_string", + -- "uuid", + -- "string_list", + "bytes", + -- "short_bytes", + "options", + -- "options_list" + "inet", + -- "consistency" + "string_map", + -- "string_multimap" } for _, buf_type in ipairs(TYPES) do local mod = require("cassandra.types."..buf_type) Buffer["read_"..buf_type] = mod.read - Buffer["write_"..buf_type] = mod.write + Buffer["repr_"..buf_type] = mod.repr + Buffer["write_"..buf_type] = function(self, val) + local repr = mod.repr(self, val) + self:write(repr) + end +end + +--- CQL Types +-- @section cql_types + +local CQL_TYPES_ = { + "raw", + -- "ascii", + -- "biging", + -- "blob", + "boolean", + -- "decimal", + -- "double", + -- "float", + "inet", + "int", + -- "list", + -- "map", + "set", + -- "text", + -- "timestamp", + -- "uuid", + -- "varchar", + -- "varint", + -- "timeuuid", + -- "tuple" +} + +for _, cql_type in ipairs(CQL_TYPES_) do + local mod = require("cassandra.types."..cql_type) + Buffer["repr_cql_"..cql_type] = function(self, ...) + local repr = mod.repr(self, ...) + return self:repr_bytes(repr) + end + Buffer["write_cql_"..cql_type] = function(self, ...) + local repr = mod.repr(self, ...) + self:write_bytes(repr) + end + Buffer["read_cql_"..cql_type] = function(self, ...) + local bytes = self:read_bytes() + local buf = Buffer(self.version, bytes) + return mod.read(buf, ...) + end +end + +local DECODER_NAMES = { + -- custom = 0x00, + [CQL_TYPES.ascii] = "raw", + [CQL_TYPES.bigint] = "bigint", + [CQL_TYPES.blob] = "raw", + [CQL_TYPES.boolean] = "boolean", + [CQL_TYPES.counter] = "counter", + -- decimal 0x06 + [CQL_TYPES.double] = "double", + [CQL_TYPES.float] = "float", + [CQL_TYPES.int] = "int", + [CQL_TYPES.text] = "raw", + [CQL_TYPES.timestamp] = "timestamp", + [CQL_TYPES.uuid] = "uuid", + [CQL_TYPES.varchar] = "raw", + [CQL_TYPES.varint] = "varint", + [CQL_TYPES.timeuuid] = "timeuuid", + [CQL_TYPES.inet] = "inet", + [CQL_TYPES.list] = "list", + [CQL_TYPES.map] = "map", + [CQL_TYPES.set] = "set", + [CQL_TYPES.udt] = "udt", + [CQL_TYPES.tuple] = "tuple" +} + +function Buffer:write_cql_value(value, assumed_type) + local infered_type + local lua_type = type(value) + + if assumed_type then + infered_type = assumed_type + elseif lua_type == "number" and math_floor(value) == value then + infered_type = CQL_TYPES.int + end + + local encoder = "write_cql_"..DECODER_NAMES[infered_type] + Buffer[encoder](self, value) +end + +function Buffer:read_cql_value(assumed_type) + local decoder = "read_cql_"..DECODER_NAMES[assumed_type.type_id] + return Buffer[decoder](self, assumed_type.value) end return Buffer diff --git a/src/cassandra/client.lua b/src/cassandra/client.lua index 447f321..e5f797f 100644 --- a/src/cassandra/client.lua +++ b/src/cassandra/client.lua @@ -6,9 +6,9 @@ local ControlConnection = require "cassandra.control_connection" --- CLIENT -- @section client -local _CLIENT = Object:extend() +local Client = Object:extend() -function _CLIENT:new(options) +function Client:new(options) options = client_options.parse(options) self.keyspace = options.keyspace self.hosts = {} @@ -28,11 +28,11 @@ local function _connect(self) self.connected = true end -function _CLIENT:execute() +function Client:execute() local err = _connect(self) if err then return err end end -return _CLIENT +return Client diff --git a/src/cassandra/client_options.lua b/src/cassandra/client_options.lua index 4bc4bd7..788b84a 100644 --- a/src/cassandra/client_options.lua +++ b/src/cassandra/client_options.lua @@ -1,4 +1,4 @@ -local utils = require "cassandra.utils" +local utils = require "cassandra.utils.table" local errors = require "cassandra.errors" --- CONST diff --git a/src/cassandra/consts.lua b/src/cassandra/consts.lua index 660e516..a725e01 100644 --- a/src/cassandra/consts.lua +++ b/src/cassandra/consts.lua @@ -2,5 +2,6 @@ return { DEFAULT_PROTOCOL_VERSION = 3, MIN_PROTOCOL_VERSION = 2, MAX_PROTOCOL_VERSION = 3, - CQL_VERSION = "3.0.0" + CQL_VERSION = "3.0.0", + DEFAULT_CQL_PORT = 9042 } diff --git a/src/cassandra/control_connection.lua b/src/cassandra/control_connection.lua index fc3a257..55bc750 100644 --- a/src/cassandra/control_connection.lua +++ b/src/cassandra/control_connection.lua @@ -1,10 +1,12 @@ --- Represent a connection from the driver to the cluster and handle events between the two +local CONSTS = require "cassandra.consts" local Object = require "cassandra.classic" local Host = require "cassandra.host" local HostConnection = require "cassandra.host_connection" local RequestHandler = require "cassandra.request_handler" local utils = require "cassandra.utils" local log = require "cassandra.log" +local requests = require "cassandra.requests" local table_insert = table.insert --- Constants @@ -28,7 +30,7 @@ function ControlConnection:init() for _, contact_point in ipairs(self.contact_points) do -- Extract port if string is of the form "host:port" local addr, port = utils.split_by_colon(contact_point) - if not port then port = 9042 end -- @TODO add this to some constant + if not port then port = CONSTS.DEFAULT_CQL_PORT end table_insert(self.hosts, Host(addr, port)) end @@ -37,6 +39,8 @@ function ControlConnection:init() return nil, err end + local err = self:refresh_hosts(any_host) + -- @TODO get peers info -- @TODO get local info -- local peers, err @@ -45,8 +49,20 @@ function ControlConnection:init() return self.hosts end -function ControlConnection:get_peers() +function ControlConnection:refresh_hosts(host) + log.debug("Refreshing local and peers info") + return self:get_peers(host) +end + +function ControlConnection:get_peers(host) + local peers_query = requests.QueryRequest(SELECT_PEERS_QUERY) + local result, err = host.connection:send(peers_query) + if err then + return err + end + local inspect = require "inspect" + print("Peers result: "..inspect(result)) end return ControlConnection diff --git a/src/cassandra/frame_reader.lua b/src/cassandra/frame_reader.lua index f37ffa3..4860b0f 100644 --- a/src/cassandra/frame_reader.lua +++ b/src/cassandra/frame_reader.lua @@ -1,8 +1,9 @@ +local Object = require "cassandra.classic" local Buffer = require "cassandra.buffer" local errors = require "cassandra.errors" local frame_header = require "cassandra.types.frame_header" +local bit = require "cassandra.utils.bit" local op_codes = frame_header.op_codes -local bit = require "bit" --- CONST -- @section constants @@ -43,45 +44,128 @@ local ERRORS_TRANSLATION = { [ERRORS.UNPREPARED] = "Unprepared" } ---- FrameHeader --- @section frameheader +local RESULT_KINDS = { + VOID = 0x01, + ROWS = 0x02, + SET_KEYSPACE = 0x03, + PREPARED = 0x04, + SCHEMA_CHANGE = 0x05 +} -local FrameReader = Buffer:extend() +local ROWS_RESULT_FLAGS = { + GLOBAL_TABLES_SPEC = 0x01, + HAS_MORE_PAGES = 0x02, + NO_METADATA = 0x04 +} -function FrameReader:new(frameHeader, raw_bytes) - self.frameHeader = frameHeader +--- ResultParser +-- @section result_parser + +local function parse_metadata(buffer) + local k_name, t_name + + local flags = buffer:read_int() + local columns_count = buffer:read_int() - FrameReader.super.new(self, raw_bytes, frameHeader.version) + local has_more_pages = bit.btest(flags, ROWS_RESULT_FLAGS.HAS_MORE_PAGES) + local has_global_table_spec = bit.btest(flags, ROWS_RESULT_FLAGS.GLOBAL_TABLES_SPEC) + + if has_global_table_spec then + k_name = buffer:read_string() + t_name = buffer:read_string() + end + + local columns = {} + for _ = 1, columns_count do + if not has_global_table_spec then + k_name = buffer:read_string() + t_name = buffer:read_string() + end + local col_name = buffer:read_string() + local col_type = buffer:read_options() -- {type_id = ...[, value_type_id = ...]} + columns[#columns + 1] = { + name = col_name, + type = col_type, + keysapce = k_name, + table = t_name + } + end + + return { + columns = columns, + columns_count = columns_count + } end -local function read_frame(self) +local RESULT_PARSERS = { + [RESULT_KINDS.ROWS] = function(buffer) + local metadata = parse_metadata(buffer) + local columns = metadata.columns + local columns_count = metadata.columns_count + local rows_count = buffer:read_int() + + local rows = { + type = "ROWS" + } + for _ = 1, rows_count do + local row = {} + for i = 1, columns_count do + print("reading column "..columns[i].name) + local value = buffer:read_cql_value(columns[i].type) + local inspect = require "inspect" + print("column "..columns[i].name.." = "..inspect(value)) + row[columns[i].name] = value + end + rows[#rows + 1] = row + end + return rows + end +} + +--- FrameHeader +-- @section frameheader + +local FrameReader = Object:extend() + +function FrameReader:new(frameHeader, body_bytes) + self.frameHeader = frameHeader + self.frameBody = Buffer(frameHeader.version, body_bytes) end -local function parse_error(self) - local code = FrameReader.super.read_integer(self) - local message = FrameReader.super.read_string(self) +local function parse_error(frameBody) + local code = frameBody:read_int() + local message = frameBody:read_string() local code_translation = ERRORS_TRANSLATION[code] return errors.ResponseError(code, code_translation, message) end -local function parse_ready(self) +local function parse_ready() return {ready = true} end +local function parse_result(frameBody) + local result_kind = frameBody:read_int() + local parser = RESULT_PARSERS[result_kind] + return parser(frameBody) +end + --- Decode a response frame -function FrameReader:read() - if self.frameHeader.op_code == nil then +function FrameReader:parse() + local op_code = self.frameHeader.op_code + if op_code == nil then error("frame header has no op_code") end - local op_code = self.frameHeader.op_code + print("response op_code: "..op_code) -- Parse frame depending on op_code if op_code == op_codes.ERROR then - return nil, parse_error(self) + return nil, parse_error(self.frameBody) elseif op_code == op_codes.READY then - return parse_ready(self) + return parse_ready(self.frameBody) + elseif op_code == op_codes.RESULT then + return parse_result(self.frameBody) end end diff --git a/src/cassandra/host_connection.lua b/src/cassandra/host_connection.lua index 1522021..9aec398 100644 --- a/src/cassandra/host_connection.lua +++ b/src/cassandra/host_connection.lua @@ -54,19 +54,24 @@ function HostConnection:new(address, port) self.protocol_version = CONSTS.DEFAULT_PROTOCOL_VERSION end +function HostConnection:decrease_version() + self.protocol_version = self.protocol_version - 1 + if self.protocol_version < CONSTS.MIN_PROTOCOL_VERSION then + error("minimum protocol version supported: ", CONSTS.MIN_PROTOCOL_VERSION) + end +end + --- Socket operations -- @section socket local function send_and_receive(self, request) - request.version = self.protocol_version - -- Send frame local bytes_sent, err = self.socket:send(request:get_full_frame()) if bytes_sent == nil then return nil, err end - -- Receive frame version + -- Receive frame version byte local frame_version_byte, err = self.socket:receive(1) if frame_version_byte == nil then return nil, err @@ -81,6 +86,8 @@ local function send_and_receive(self, request) end local frameHeader = FrameHeader.from_raw_bytes(frame_version_byte, header_bytes) + print("BODY BYTES: "..frameHeader.body_length) + print("OP_CODE: "..frameHeader.op_code) -- Receive frame body local body_bytes @@ -93,22 +100,13 @@ local function send_and_receive(self, request) local frameReader = FrameReader(frameHeader, body_bytes) - return frameReader:read() + return frameReader:parse() end ---- Determine the protocol version to use and send the STARTUP request -local function startup(self) - log.debug("Startup request. Trying to use protocol v"..self.protocol_version) - - local startup_req = requests.StartupRequest() - return send_and_receive(self, startup_req) -end -function HostConnection:decrease_version() - self.protocol_version = self.protocol_version - 1 - if self.protocol_version < CONSTS.MIN_PROTOCOL_VERSION then - error("minimum protocol version supported: ", CONSTS.MIN_PROTOCOL_VERSION) - end +function HostConnection:send(request) + request:set_version(self.protocol_version) + return send_and_receive(self, request) end function HostConnection:close() @@ -119,6 +117,14 @@ function HostConnection:close() return res == 1 end +--- Determine the protocol version to use and send the STARTUP request +local function startup(self) + log.debug("Startup request. Trying to use protocol v"..self.protocol_version) + + local startup_req = requests.StartupRequest() + return self.send(self, startup_req) +end + function HostConnection:open() local address = self.address..":"..self.port new_socket(self) diff --git a/src/cassandra/requests.lua b/src/cassandra/requests.lua index 2c596d7..87ca3f5 100644 --- a/src/cassandra/requests.lua +++ b/src/cassandra/requests.lua @@ -1,4 +1,5 @@ local CONSTS = require "cassandra.consts" +local Object = require "cassandra.classic" local Buffer = require "cassandra.buffer" local frame_header = require "cassandra.types.frame_header" @@ -8,22 +9,41 @@ local FrameHeader = frame_header.FrameHeader --- Request -- @section request -local Request = Buffer:extend() +local Request = Object:extend() -function Request:new(options) - if options == nil then options = {} end +function Request:new(op_code) + self.version = nil + self.flags = 0 + self.op_code = op_code + self.frameBody = Buffer() -- no version + self.built = false - self.version = options.version -- to be set by host_connection.lua before being sent - self.op_code = options.op_code + Request.super.new(self) +end + +function Request:set_version(version) + self.version = version + self.frameBody.version = version +end - Request.super.new(self, nil, self.version) +function Request:build() + error("mest be implemented") end -function Request:get_full_frame(flags) - if not self.op_code then error("Request#write() has no op_code") end +function Request:get_full_frame() + if not self.op_code then error("Request#write() has no op_code attribute") end + if not self.version then error("Request#write() has no version attribute") end + + if not self.built then + self:build() + self.built = true + end - local frameHeader = FrameHeader(self.version, flags, self.op_code, self.len) - return frameHeader:write()..Request.super.write(self) + local frameHeader = FrameHeader(self.version, self.flags, self.op_code, self.frameBody.len) + local header = frameHeader:dump() + local body = self.frameBody:dump() + + return header..body end --- StartupRequest @@ -31,16 +51,39 @@ end local StartupRequest = Request:extend() -function StartupRequest:new(options) - if options == nil then options = {} end - options.op_code = op_codes.STARTUP - StartupRequest.super.new(self, options) - StartupRequest.super.write_string_map(self, { +function StartupRequest:new() + StartupRequest.super.new(self, op_codes.STARTUP) +end + +function StartupRequest:build() + self.frameBody:write_string_map({ CQL_VERSION = CONSTS.CQL_VERSION }) end +--- QueryRequest +-- @section query_request + +local QueryRequest = Request:extend() + +function QueryRequest:new(query, params, options) + self.query = query + self.params = params + self.options = options + QueryRequest.super.new(self, op_codes.QUERY) +end + +function QueryRequest:build() + -- v2: + -- [...][][][] + -- v3: + -- [[name_1]...[name_n]][][][][] + self.frameBody:write_long_string(self.query) + self.frameBody:write_short(0x0001) -- @TODO support consistency_level + self.frameBody:write_byte(0) -- @TODO support query flags +end + return { - Request = Request, - StartupRequest = StartupRequest + StartupRequest = StartupRequest, + QueryRequest = QueryRequest } diff --git a/src/cassandra/types/boolean.lua b/src/cassandra/types/boolean.lua index da61611..7e29085 100644 --- a/src/cassandra/types/boolean.lua +++ b/src/cassandra/types/boolean.lua @@ -1,12 +1,13 @@ return { - write = function(self, val) + repr = function(self, val) if val then - self:write_byte(1) + return self:repr_byte(1) else - self:write_byte(0) + return self:repr_byte(0) end end, - read = function(self) - return self:read_byte() == 1 + read = function(buffer) + local byte = buffer:read_byte() + return byte == 1 end } diff --git a/src/cassandra/types/byte.lua b/src/cassandra/types/byte.lua index a3489ee..431796d 100644 --- a/src/cassandra/types/byte.lua +++ b/src/cassandra/types/byte.lua @@ -2,11 +2,10 @@ local string_char = string.char local string_byte = string.byte return { - write = function(self, val) - self:write_bytes(string_char(val)) + repr = function(self, val) + return string_char(val) end, read = function(self) - local byte = self:read_bytes(1) - return string_byte(byte) + return string_byte(self:read(1)) end } diff --git a/src/cassandra/types/bytes.lua b/src/cassandra/types/bytes.lua new file mode 100644 index 0000000..432fbf6 --- /dev/null +++ b/src/cassandra/types/bytes.lua @@ -0,0 +1,11 @@ +local int = require "cassandra.types.int" + +return { + repr = function(self, val) + return int.repr(nil, #val)..val + end, + read = function(self) + local n_bytes = int.read(self) + return self:read(n_bytes) + end +} diff --git a/src/cassandra/types/cql_types.lua b/src/cassandra/types/cql_types.lua new file mode 100644 index 0000000..1f5564b --- /dev/null +++ b/src/cassandra/types/cql_types.lua @@ -0,0 +1,24 @@ +return { + custom = 0x00, + ascii = 0x01, + bigint = 0x02, + blob = 0x03, + boolean = 0x04, + counter = 0x05, + decimal = 0x06, + double = 0x07, + float = 0x08, + int = 0x09, + text = 0x0A, + timestamp = 0x0B, + uuid = 0x0C, + varchar = 0x0D, + varint = 0x0E, + timeuuid = 0x0F, + inet = 0x10, + list = 0x20, + map = 0x21, + set = 0x22, + udt = 0x30, + tuple = 0x31 +} diff --git a/src/cassandra/types/frame_header.lua b/src/cassandra/types/frame_header.lua index e44c771..61a6106 100644 --- a/src/cassandra/types/frame_header.lua +++ b/src/cassandra/types/frame_header.lua @@ -1,5 +1,5 @@ local utils = require "cassandra.utils" -local bit = require "bit" +local bit = require "cassandra.utils.bit" local Buffer = require "cassandra.buffer" --- CONST @@ -57,27 +57,27 @@ function FrameHeader:new(version, flags, op_code, body_length) self.stream_id = 0 -- @TODO support streaming self.body_length = body_length - self.super.new(self, nil, version) + self.super.new(self, version) end -function FrameHeader:write() - self.super.write_byte(self, VERSION_CODES:get("REQUEST", self.version)) - self.super.write_byte(self, self.flags) -- @TODO find a more secure way +function FrameHeader:dump() + FrameHeader.super.write_byte(self, VERSION_CODES:get("REQUEST", self.version)) + FrameHeader.super.write_byte(self, self.flags) -- @TODO find a more secure way if self.version < 3 then - self.super.write_byte(self, self.stream_id) + FrameHeader.super.write_byte(self, self.stream_id) else - self.super.write_short(self, self.stream_id) + FrameHeader.super.write_short(self, self.stream_id) end - self.super.write_byte(self, self.op_code) -- @TODO find a more secure way - self.super.write_integer(self, self.body_length) + FrameHeader.super.write_byte(self, self.op_code) -- @TODO find a more secure way + FrameHeader.super.write_int(self, self.body_length) - return self.super.write(self) + return FrameHeader.super.dump(self) end function FrameHeader.version_from_byte(byte) - local buf = Buffer(byte) + local buf = Buffer(nil, byte) return bit.band(buf:read_byte(), 0x7F) end @@ -90,10 +90,11 @@ function FrameHeader.size_from_byte(version_byte) end function FrameHeader.from_raw_bytes(version_byte, raw_bytes) - local buffer = Buffer(raw_bytes) local version = FrameHeader.version_from_byte(version_byte) - buffer.version = version + local buffer = Buffer(version, raw_bytes) local flags = buffer:read_byte() + print("VERSION: "..version) + print("FLAGS: "..flags) local stream_id if version < 3 then @@ -103,7 +104,7 @@ function FrameHeader.from_raw_bytes(version_byte, raw_bytes) end local op_code = buffer:read_byte() - local body_length = buffer:read_integer() + local body_length = buffer:read_int() return FrameHeader(version, flags, op_code, body_length) end diff --git a/src/cassandra/types/inet.lua b/src/cassandra/types/inet.lua new file mode 100644 index 0000000..643f63e --- /dev/null +++ b/src/cassandra/types/inet.lua @@ -0,0 +1,57 @@ +local string_gmatch = string.gmatch +local string_rep = string.rep +local string_sub = string.sub +local string_byte = string.byte +local string_format = string.format +local tonumber = tonumber +local table_insert = table.insert +local table_concat = table.concat + +return { + repr = function(self, value) + local t = {} + local hexadectets = {} + local ip = value:lower():gsub("::",":0000:") + + if value:match(":") then + -- ipv6 + for hdt in string_gmatch(ip, "[%x]+") do + -- fill up hexadectets with 0 so all are 4 digits long + hexadectets[#hexadectets + 1] = string_rep("0", 4 - #hdt)..hdt + end + for i, hdt in ipairs(hexadectets) do + while hdt == "0000" and #hexadectets < 8 do + table_insert(hexadectets, i + 1, "0000") + end + for j = 1, 4, 2 do + table_insert(t, self:repr_byte(tonumber(string_sub(hdt, j, j + 1), 16))) + end + end + else + -- ipv4 + for d in string_gmatch(value, "(%d+)") do + table_insert(t, self:repr_byte(d)) + end + end + + return table_concat(t) + end, + read = function(buffer) + local bytes = buffer:dump() + buffer = {} + if #bytes == 16 then + -- ipv6 + for i = 1, #bytes, 2 do + buffer[#buffer + 1] = string_format("%02x", string_byte(bytes, i))..string_format("%02x", string_byte(bytes, i + 1)) + end + return table_concat(buffer, ":") + else + -- ipv4 + for i = 1, #bytes do + buffer[#buffer + 1] = string_format("%d", string_byte(bytes, i)) + end + end + + return table_concat(buffer, ".") + end +} diff --git a/src/cassandra/types/int.lua b/src/cassandra/types/int.lua new file mode 100644 index 0000000..f812b51 --- /dev/null +++ b/src/cassandra/types/int.lua @@ -0,0 +1,11 @@ +local utils = require "cassandra.utils" + +return { + repr = function(self, val) + return utils.big_endian_representation(val, 4) + end, + read = function(buffer) + local bytes = buffer:read(4) + return utils.string_to_number(bytes, true) + end +} diff --git a/src/cassandra/types/integer.lua b/src/cassandra/types/integer.lua deleted file mode 100644 index 605deec..0000000 --- a/src/cassandra/types/integer.lua +++ /dev/null @@ -1,11 +0,0 @@ -local utils = require "cassandra.utils" - -return { - read = function(self) - local bytes = self:read_bytes(4) - return utils.string_to_number(bytes, true) - end, - write = function(self, val) - self:write_bytes(utils.big_endian_representation(val, 4)) - end -} diff --git a/src/cassandra/types/long_string.lua b/src/cassandra/types/long_string.lua new file mode 100644 index 0000000..9666fb0 --- /dev/null +++ b/src/cassandra/types/long_string.lua @@ -0,0 +1,9 @@ +return { + repr = function(self, str) + return self:repr_int(#str)..str + end, + read = function(buffer) + local n_bytes = buffer:read_int() + return buffer:read(n_bytes) + end +} diff --git a/src/cassandra/types/options.lua b/src/cassandra/types/options.lua new file mode 100644 index 0000000..125e91e --- /dev/null +++ b/src/cassandra/types/options.lua @@ -0,0 +1,14 @@ +local CQL_TYPES = require "cassandra.types.cql_types" + +return { + read = function(buffer) + local type_id = buffer:read_short() + local type_value + if type_id == CQL_TYPES.set then + type_value = buffer:read_options() + end + + -- @TODO support non-native types (custom, map, list, set, UDT, tuple) + return {type_id = type_id, value = type_value} + end +} diff --git a/src/cassandra/types/raw.lua b/src/cassandra/types/raw.lua new file mode 100644 index 0000000..1f6a7ec --- /dev/null +++ b/src/cassandra/types/raw.lua @@ -0,0 +1,8 @@ +return { + repr = function(self, bytes) + return bytes + end, + read = function(buffer) + return buffer:dump() + end +} diff --git a/src/cassandra/types/set.lua b/src/cassandra/types/set.lua new file mode 100644 index 0000000..be7d5c0 --- /dev/null +++ b/src/cassandra/types/set.lua @@ -0,0 +1,27 @@ +return { + repr = function(self, set) + local n + if self.version < 3 then + n = self:repr_short(#set) + else + n = self:repr_int(#set) + end + for _, val in ipairs(set) do + -- @TODO write_value infering the type + end + end, + read = function(buffer, value_type) + local n + local set = {} + if buffer.version < 3 then + n = buffer:read_short() + else + n = buffer:read_int() + end + for _ = 1, n do + set[#set + 1] = buffer:read_cql_value(value_type) + end + + return set + end +} diff --git a/src/cassandra/types/short.lua b/src/cassandra/types/short.lua index fd9c35a..70a190c 100644 --- a/src/cassandra/types/short.lua +++ b/src/cassandra/types/short.lua @@ -1,11 +1,11 @@ local utils = require "cassandra.utils" return { - write = function(self, val) - self:write_bytes(utils.big_endian_representation(val, 2)) + repr = function(self, val) + return utils.big_endian_representation(val, 2) end, - read = function(self) - local bytes = self:read_bytes(2) + read = function(buffer) + local bytes = buffer:read(2) return utils.string_to_number(bytes, true) end } diff --git a/src/cassandra/types/string.lua b/src/cassandra/types/string.lua index d49e004..93a7b41 100644 --- a/src/cassandra/types/string.lua +++ b/src/cassandra/types/string.lua @@ -1,10 +1,9 @@ return { - write = function(self, str) - self:write_short(#str) - self:write_bytes(str) + repr = function(self, str) + return self:repr_short(#str)..str end, - read = function(self) - local n_bytes = self:read_short() - return self:read_bytes(n_bytes) + read = function(buffer) + local n_bytes = buffer:read_short() + return buffer:read(n_bytes) end } diff --git a/src/cassandra/types/string_map.lua b/src/cassandra/types/string_map.lua index a9e9f88..24dc65d 100644 --- a/src/cassandra/types/string_map.lua +++ b/src/cassandra/types/string_map.lua @@ -1,21 +1,25 @@ +local table_concat = table.concat + return { - write = function(self, map) - local n = #map + repr = function(self, map) + local t = {} + local n = 0 for k, v in pairs(map) do n = n + 1 end - self:write_short(n) + t[1] = self:repr_short(n) for k, v in pairs(map) do - self:write_string(k) - self:write_string(v) + t[#t + 1] = self:repr_string(k) + t[#t + 1] = self:repr_string(v) end + return table_concat(t) end, - read = function(self) + read = function(buffer) local map = {} - local n_strings = self:read_short() + local n_strings = buffer:read_short() for _ = 1, n_strings do - local key = self:read_string() - local value = self:read_string() + local key = buffer:read_string() + local value = buffer:read_string() map[key] = value end return map diff --git a/src/cassandra/utils/bit.lua b/src/cassandra/utils/bit.lua new file mode 100644 index 0000000..3a6f96f --- /dev/null +++ b/src/cassandra/utils/bit.lua @@ -0,0 +1,545 @@ +--[[ + +LUA MODULE + + bit.numberlua - Bitwise operations implemented in pure Lua as numbers, + with Lua 5.2 'bit32' and (LuaJIT) LuaBitOp 'bit' compatibility interfaces. + +SYNOPSIS + + local bit = require 'bit.numberlua' + print(bit.band(0xff00ff00, 0x00ff00ff)) --> 0xffffffff + + -- Interface providing strong Lua 5.2 'bit32' compatibility + local bit32 = require 'bit.numberlua'.bit32 + assert(bit32.band(-1) == 0xffffffff) + + -- Interface providing strong (LuaJIT) LuaBitOp 'bit' compatibility + local bit = require 'bit.numberlua'.bit + assert(bit.tobit(0xffffffff) == -1) + +DESCRIPTION + + This library implements bitwise operations entirely in Lua. + This module is typically intended if for some reasons you don't want + to or cannot install a popular C based bit library like BitOp 'bit' [1] + (which comes pre-installed with LuaJIT) or 'bit32' (which comes + pre-installed with Lua 5.2) but want a similar interface. + + This modules represents bit arrays as non-negative Lua numbers. [1] + It can represent 32-bit bit arrays when Lua is compiled + with lua_Number as double-precision IEEE 754 floating point. + + The module is nearly the most efficient it can be but may be a few times + slower than the C based bit libraries and is orders or magnitude + slower than LuaJIT bit operations, which compile to native code. Therefore, + this library is inferior in performane to the other modules. + + The `xor` function in this module is based partly on Roberto Ierusalimschy's + post in http://lua-users.org/lists/lua-l/2002-09/msg00134.html . + + The included BIT.bit32 and BIT.bit sublibraries aims to provide 100% + compatibility with the Lua 5.2 "bit32" and (LuaJIT) LuaBitOp "bit" library. + This compatbility is at the cost of some efficiency since inputted + numbers are normalized and more general forms (e.g. multi-argument + bitwise operators) are supported. + +STATUS + + WARNING: Not all corner cases have been tested and documented. + Some attempt was made to make these similar to the Lua 5.2 [2] + and LuaJit BitOp [3] libraries, but this is not fully tested and there + are currently some differences. Addressing these differences may + be improved in the future but it is not yet fully determined how to + resolve these differences. + + The BIT.bit32 library passes the Lua 5.2 test suite (bitwise.lua) + http://www.lua.org/tests/5.2/ . The BIT.bit library passes the LuaBitOp + test suite (bittest.lua). However, these have not been tested on + platforms with Lua compiled with 32-bit integer numbers. + +API + + BIT.tobit(x) --> z + + Similar to function in BitOp. + + BIT.tohex(x, n) + + Similar to function in BitOp. + + BIT.band(x, y) --> z + + Similar to function in Lua 5.2 and BitOp but requires two arguments. + + BIT.bor(x, y) --> z + + Similar to function in Lua 5.2 and BitOp but requires two arguments. + + BIT.bxor(x, y) --> z + + Similar to function in Lua 5.2 and BitOp but requires two arguments. + + BIT.bnot(x) --> z + + Similar to function in Lua 5.2 and BitOp. + + BIT.lshift(x, disp) --> z + + Similar to function in Lua 5.2 (warning: BitOp uses unsigned lower 5 bits of shift), + + BIT.rshift(x, disp) --> z + + Similar to function in Lua 5.2 (warning: BitOp uses unsigned lower 5 bits of shift), + + BIT.extract(x, field [, width]) --> z + + Similar to function in Lua 5.2. + + BIT.replace(x, v, field, width) --> z + + Similar to function in Lua 5.2. + + BIT.bswap(x) --> z + + Similar to function in Lua 5.2. + + BIT.rrotate(x, disp) --> z + BIT.ror(x, disp) --> z + + Similar to function in Lua 5.2 and BitOp. + + BIT.lrotate(x, disp) --> z + BIT.rol(x, disp) --> z + + Similar to function in Lua 5.2 and BitOp. + + BIT.arshift + + Similar to function in Lua 5.2 and BitOp. + + BIT.btest + + Similar to function in Lua 5.2 with requires two arguments. + + BIT.bit32 + + This table contains functions that aim to provide 100% compatibility + with the Lua 5.2 "bit32" library. + + bit32.arshift (x, disp) --> z + bit32.band (...) --> z + bit32.bnot (x) --> z + bit32.bor (...) --> z + bit32.btest (...) --> true | false + bit32.bxor (...) --> z + bit32.extract (x, field [, width]) --> z + bit32.replace (x, v, field [, width]) --> z + bit32.lrotate (x, disp) --> z + bit32.lshift (x, disp) --> z + bit32.rrotate (x, disp) --> z + bit32.rshift (x, disp) --> z + + BIT.bit + + This table contains functions that aim to provide 100% compatibility + with the LuaBitOp "bit" library (from LuaJIT). + + bit.tobit(x) --> y + bit.tohex(x [,n]) --> y + bit.bnot(x) --> y + bit.bor(x1 [,x2...]) --> y + bit.band(x1 [,x2...]) --> y + bit.bxor(x1 [,x2...]) --> y + bit.lshift(x, n) --> y + bit.rshift(x, n) --> y + bit.arshift(x, n) --> y + bit.rol(x, n) --> y + bit.ror(x, n) --> y + bit.bswap(x) --> y + +DEPENDENCIES + + None (other than Lua 5.1 or 5.2). + +DOWNLOAD/INSTALLATION + + If using LuaRocks: + luarocks install lua-bit-numberlua + + Otherwise, download . + Alternately, if using git: + git clone git://github.com/davidm/lua-bit-numberlua.git + cd lua-bit-numberlua + Optionally unpack: + ./util.mk + or unpack and install in LuaRocks: + ./util.mk install + +REFERENCES + + [1] http://lua-users.org/wiki/FloatingPoint + [2] http://www.lua.org/manual/5.2/ + [3] http://bitop.luajit.org/ + +LICENSE + + (c) 2008-2011 David Manura. Licensed under the same terms as Lua (MIT). + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + (end license) + +--]] + +local M = {_TYPE='module', _NAME='bit.numberlua', _VERSION='0.3.1.20120131'} + +local floor = math.floor + +local MOD = 2^32 +local MODM = MOD-1 + +local function memoize(f) + local mt = {} + local t = setmetatable({}, mt) + function mt:__index(k) + local v = f(k); t[k] = v + return v + end + return t +end + +local function make_bitop_uncached(t, m) + local function bitop(a, b) + local res,p = 0,1 + while a ~= 0 and b ~= 0 do + local am, bm = a%m, b%m + res = res + t[am][bm]*p + a = (a - am) / m + b = (b - bm) / m + p = p*m + end + res = res + (a+b)*p + return res + end + return bitop +end + +local function make_bitop(t) + local op1 = make_bitop_uncached(t,2^1) + local op2 = memoize(function(a) + return memoize(function(b) + return op1(a, b) + end) + end) + return make_bitop_uncached(op2, 2^(t.n or 1)) +end + +-- ok? probably not if running on a 32-bit int Lua number type platform +function M.tobit(x) + return x % 2^32 +end + +M.bxor = make_bitop {[0]={[0]=0,[1]=1},[1]={[0]=1,[1]=0}, n=4} +local bxor = M.bxor + +function M.bnot(a) return MODM - a end +local bnot = M.bnot + +function M.band(a,b) return ((a+b) - bxor(a,b))/2 end +local band = M.band + +function M.bor(a,b) return MODM - band(MODM - a, MODM - b) end +local bor = M.bor + +local lshift, rshift -- forward declare + +function M.rshift(a,disp) -- Lua5.2 insipred + if disp < 0 then return lshift(a,-disp) end + return floor(a % 2^32 / 2^disp) +end +rshift = M.rshift + +function M.lshift(a,disp) -- Lua5.2 inspired + if disp < 0 then return rshift(a,-disp) end + return (a * 2^disp) % 2^32 +end +lshift = M.lshift + +function M.tohex(x, n) -- BitOp style + n = n or 8 + local up + if n <= 0 then + if n == 0 then return '' end + up = true + n = - n + end + x = band(x, 16^n-1) + return ('%0'..n..(up and 'X' or 'x')):format(x) +end +local tohex = M.tohex + +function M.extract(n, field, width) -- Lua5.2 inspired + width = width or 1 + return band(rshift(n, field), 2^width-1) +end +local extract = M.extract + +function M.replace(n, v, field, width) -- Lua5.2 inspired + width = width or 1 + local mask1 = 2^width-1 + v = band(v, mask1) -- required by spec? + local mask = bnot(lshift(mask1, field)) + return band(n, mask) + lshift(v, field) +end +local replace = M.replace + +function M.bswap(x) -- BitOp style + local a = band(x, 0xff); x = rshift(x, 8) + local b = band(x, 0xff); x = rshift(x, 8) + local c = band(x, 0xff); x = rshift(x, 8) + local d = band(x, 0xff) + return lshift(lshift(lshift(a, 8) + b, 8) + c, 8) + d +end +local bswap = M.bswap + +function M.rrotate(x, disp) -- Lua5.2 inspired + disp = disp % 32 + local low = band(x, 2^disp-1) + return rshift(x, disp) + lshift(low, 32-disp) +end +local rrotate = M.rrotate + +function M.lrotate(x, disp) -- Lua5.2 inspired + return rrotate(x, -disp) +end +local lrotate = M.lrotate + +M.rol = M.lrotate -- LuaOp inspired +M.ror = M.rrotate -- LuaOp insipred + + +function M.arshift(x, disp) -- Lua5.2 inspired + local z = rshift(x, disp) + if x >= 0x80000000 then z = z + lshift(2^disp-1, 32-disp) end + return z +end +local arshift = M.arshift + +function M.btest(x, y) -- Lua5.2 inspired + return band(x, y) ~= 0 +end + +-- +-- Start Lua 5.2 "bit32" compat section. +-- + +M.bit32 = {} -- Lua 5.2 'bit32' compatibility + + +local function bit32_bnot(x) + return (-1 - x) % MOD +end +M.bit32.bnot = bit32_bnot + +local function bit32_bxor(a, b, c, ...) + local z + if b then + a = a % MOD + b = b % MOD + z = bxor(a, b) + if c then + z = bit32_bxor(z, c, ...) + end + return z + elseif a then + return a % MOD + else + return 0 + end +end +M.bit32.bxor = bit32_bxor + +local function bit32_band(a, b, c, ...) + local z + if b then + a = a % MOD + b = b % MOD + z = ((a+b) - bxor(a,b)) / 2 + if c then + z = bit32_band(z, c, ...) + end + return z + elseif a then + return a % MOD + else + return MODM + end +end +M.bit32.band = bit32_band + +local function bit32_bor(a, b, c, ...) + local z + if b then + a = a % MOD + b = b % MOD + z = MODM - band(MODM - a, MODM - b) + if c then + z = bit32_bor(z, c, ...) + end + return z + elseif a then + return a % MOD + else + return 0 + end +end +M.bit32.bor = bit32_bor + +function M.bit32.btest(...) + return bit32_band(...) ~= 0 +end + +function M.bit32.lrotate(x, disp) + return lrotate(x % MOD, disp) +end + +function M.bit32.rrotate(x, disp) + return rrotate(x % MOD, disp) +end + +function M.bit32.lshift(x,disp) + if disp > 31 or disp < -31 then return 0 end + return lshift(x % MOD, disp) +end + +function M.bit32.rshift(x,disp) + if disp > 31 or disp < -31 then return 0 end + return rshift(x % MOD, disp) +end + +function M.bit32.arshift(x,disp) + x = x % MOD + if disp >= 0 then + if disp > 31 then + return (x >= 0x80000000) and MODM or 0 + else + local z = rshift(x, disp) + if x >= 0x80000000 then z = z + lshift(2^disp-1, 32-disp) end + return z + end + else + return lshift(x, -disp) + end +end + +function M.bit32.extract(x, field, ...) + local width = ... or 1 + if field < 0 or field > 31 or width < 0 or field+width > 32 then error 'out of range' end + x = x % MOD + return extract(x, field, ...) +end + +function M.bit32.replace(x, v, field, ...) + local width = ... or 1 + if field < 0 or field > 31 or width < 0 or field+width > 32 then error 'out of range' end + x = x % MOD + v = v % MOD + return replace(x, v, field, ...) +end + + +-- +-- Start LuaBitOp "bit" compat section. +-- + +M.bit = {} -- LuaBitOp "bit" compatibility + +function M.bit.tobit(x) + x = x % MOD + if x >= 0x80000000 then x = x - MOD end + return x +end +local bit_tobit = M.bit.tobit + +function M.bit.tohex(x, ...) + return tohex(x % MOD, ...) +end + +function M.bit.bnot(x) + return bit_tobit(bnot(x % MOD)) +end + +local function bit_bor(a, b, c, ...) + if c then + return bit_bor(bit_bor(a, b), c, ...) + elseif b then + return bit_tobit(bor(a % MOD, b % MOD)) + else + return bit_tobit(a) + end +end +M.bit.bor = bit_bor + +local function bit_band(a, b, c, ...) + if c then + return bit_band(bit_band(a, b), c, ...) + elseif b then + return bit_tobit(band(a % MOD, b % MOD)) + else + return bit_tobit(a) + end +end +M.bit.band = bit_band + +local function bit_bxor(a, b, c, ...) + if c then + return bit_bxor(bit_bxor(a, b), c, ...) + elseif b then + return bit_tobit(bxor(a % MOD, b % MOD)) + else + return bit_tobit(a) + end +end +M.bit.bxor = bit_bxor + +function M.bit.lshift(x, n) + return bit_tobit(lshift(x % MOD, n % 32)) +end + +function M.bit.rshift(x, n) + return bit_tobit(rshift(x % MOD, n % 32)) +end + +function M.bit.arshift(x, n) + return bit_tobit(arshift(x % MOD, n % 32)) +end + +function M.bit.rol(x, n) + return bit_tobit(lrotate(x % MOD, n % 32)) +end + +function M.bit.ror(x, n) + return bit_tobit(rrotate(x % MOD, n % 32)) +end + +function M.bit.bswap(x) + return bit_tobit(bswap(x % MOD)) +end + +return M diff --git a/src/cassandra/utils/buffer.lua b/src/cassandra/utils/buffer.lua index 9f705d7..526c359 100644 --- a/src/cassandra/utils/buffer.lua +++ b/src/cassandra/utils/buffer.lua @@ -5,24 +5,25 @@ local table_concat = table.concat local Buffer = Object:extend() -function Buffer:new(str, version) - self.version = version -- protocol version +function Buffer:new(version, str) + self.version = version -- protocol version for properly encoding types self.str = str and str or "" self.pos = 1 -- lua indexes start at 1, remember? self.len = #self.str end -function Buffer:write() +function Buffer:dump() return self.str end -function Buffer:write_bytes(bytes) +function Buffer:write(bytes) self.str = self.str..bytes self.len = self.len + #bytes self.pos = self.len end -function Buffer:read_bytes(n_bytes_to_read) +function Buffer:read(n_bytes_to_read) + if n_bytes_to_read < 1 then return "" end local last_index = n_bytes_to_read ~= nil and self.pos + n_bytes_to_read - 1 or -1 local bytes = string_sub(self.str, self.pos, last_index) self.pos = self.pos + #bytes @@ -30,7 +31,13 @@ function Buffer:read_bytes(n_bytes_to_read) end function Buffer.from_buffer(buffer) - return Buffer(buffer:write(), buffer.version) + return Buffer(buffer.version, buffer:dump()) +end + +function Buffer.copy(buffer) + local b = Buffer(buffer.version, buffer:dump()) + b.pos = buffer.pos + return b end return Buffer diff --git a/src/cassandra/utils/table.lua b/src/cassandra/utils/table.lua new file mode 100644 index 0000000..6bf82bc --- /dev/null +++ b/src/cassandra/utils/table.lua @@ -0,0 +1,20 @@ +local _M = {} + +function _M.extend_table(defaults, values) + for k in pairs(defaults) do + if values[k] == nil then + values[k] = defaults[k] + end + end +end + +function _M.is_array(t) + local i = 0 + for _ in pairs(t) do + i = i + 1 + if t[i] == nil and t[tostring(i)] == nil then return false end + end + return true +end + +return _M From 6c32747023eeda498f8e3519d08f80aad65adf4e Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Wed, 4 Nov 2015 01:21:40 -0800 Subject: [PATCH 08/78] feat(rewrite) docker-compose.yml for test clusters --- docker-compose.yml | 47 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4357cb4 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,47 @@ +2.0_cass_1: + image: cassandra:2.0 + container_name: 2.0_cass_1 + ports: + - "9001:9042" +2.0_cass_2: + image: cassandra:2.0 + container_name: 2.0_cass_2 + links: + - 2.0_cass_1 + environment: + - CASSANDRA_SEEDS=2.0_cass_1 + ports: + - "9002:9042" +2.0_cass_3: + image: cassandra:2.0 + container_name: 2.0_cass_3 + links: + - 2.0_cass_1 + environment: + - CASSANDRA_SEEDS=2.0_cass_1 + ports: + - "9003:9042" + +2.1_cass_1: + image: cassandra:2.1 + container_name: 2.1_cass_1 + ports: + - "9101:9042" +2.1_cass_2: + image: cassandra:2.1 + container_name: 2.1_cass_2 + links: + - 2.1_cass_1 + environment: + - CASSANDRA_SEEDS=2.1_cass_1 + ports: + - "9102:9042" +2.1_cass_3: + image: cassandra:2.1 + container_name: 2.1_cass_3 + links: + - 2.1_cass_1 + environment: + - CASSANDRA_SEEDS=2.1_cass_1 + ports: + - "9103:9042" From f6f9f1cd7caba7d4651fa4f549005d367accd449 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Wed, 4 Nov 2015 01:30:11 -0800 Subject: [PATCH 09/78] refactor(rewrite) move utils to utils folder --- spec/rewrite_unit/utils_spec.lua | 8 +++---- src/cassandra/types/frame_header.lua | 2 +- src/cassandra/types/int.lua | 2 +- src/cassandra/types/short.lua | 2 +- src/cassandra/utils/number.lua | 32 ++++++++++++++++++++++++++++ src/cassandra/utils/string.lua | 9 ++++++++ src/cassandra/utils/table.lua | 20 +++++++++++++++++ 7 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 src/cassandra/utils/number.lua create mode 100644 src/cassandra/utils/string.lua diff --git a/spec/rewrite_unit/utils_spec.lua b/spec/rewrite_unit/utils_spec.lua index d81b3dd..685ff2b 100644 --- a/spec/rewrite_unit/utils_spec.lua +++ b/spec/rewrite_unit/utils_spec.lua @@ -1,6 +1,6 @@ -local utils = require "cassandra.utils" +local table_utils = require "cassandra.utils.table" -describe("utils", function() +describe("table_utils", function() describe("const_mt", function() local VERSION_CODES = { @@ -15,7 +15,7 @@ describe("utils", function() SOME_V3_ONLY = 3333 } } - setmetatable(VERSION_CODES, utils.const_mt) + setmetatable(VERSION_CODES, table_utils.const_mt) local FLAGS = { COMPRESSION = 1, @@ -24,7 +24,7 @@ describe("utils", function() CUSTOM_PAYLOAD = 4 } } - setmetatable(FLAGS, utils.const_mt) + setmetatable(FLAGS, table_utils.const_mt) describe("#get()", function() it("should get most recent version of a constant", function() diff --git a/src/cassandra/types/frame_header.lua b/src/cassandra/types/frame_header.lua index 61a6106..6be52c2 100644 --- a/src/cassandra/types/frame_header.lua +++ b/src/cassandra/types/frame_header.lua @@ -1,4 +1,4 @@ -local utils = require "cassandra.utils" +local utils = require "cassandra.utils.table" local bit = require "cassandra.utils.bit" local Buffer = require "cassandra.buffer" diff --git a/src/cassandra/types/int.lua b/src/cassandra/types/int.lua index f812b51..c7e907b 100644 --- a/src/cassandra/types/int.lua +++ b/src/cassandra/types/int.lua @@ -1,4 +1,4 @@ -local utils = require "cassandra.utils" +local utils = require "cassandra.utils.number" return { repr = function(self, val) diff --git a/src/cassandra/types/short.lua b/src/cassandra/types/short.lua index 70a190c..bbad5c0 100644 --- a/src/cassandra/types/short.lua +++ b/src/cassandra/types/short.lua @@ -1,4 +1,4 @@ -local utils = require "cassandra.utils" +local utils = require "cassandra.utils.number" return { repr = function(self, val) diff --git a/src/cassandra/utils/number.lua b/src/cassandra/utils/number.lua new file mode 100644 index 0000000..f166fc0 --- /dev/null +++ b/src/cassandra/utils/number.lua @@ -0,0 +1,32 @@ +local _M = {} + +function _M.big_endian_representation(num, bytes) + if num < 0 then + -- 2's complement + num = math.pow(0x100, bytes) + num + end + local t = {} + while num > 0 do + local rest = math.fmod(num, 0x100) + table.insert(t, 1, string.char(rest)) + num = (num-rest) / 0x100 + end + local padding = string.rep(string.char(0), bytes - #t) + return padding .. table.concat(t) +end + +function _M.string_to_number(str, signed) + local number = 0 + local exponent = 1 + for i = #str, 1, -1 do + number = number + string.byte(str, i) * exponent + exponent = exponent * 256 + end + if signed and number > exponent / 2 then + -- 2's complement + number = number - exponent + end + return number +end + +return _M diff --git a/src/cassandra/utils/string.lua b/src/cassandra/utils/string.lua new file mode 100644 index 0000000..d1bcf2d --- /dev/null +++ b/src/cassandra/utils/string.lua @@ -0,0 +1,9 @@ +local _M = {} + +function _M.split_by_colon(str) + local fields = {} + str:gsub("([^:]+)", function(c) fields[#fields+1] = c end) + return fields[1], fields[2] +end + +return _M diff --git a/src/cassandra/utils/table.lua b/src/cassandra/utils/table.lua index 6bf82bc..084d6fd 100644 --- a/src/cassandra/utils/table.lua +++ b/src/cassandra/utils/table.lua @@ -1,3 +1,5 @@ +local CONSTS = require "cassandra.consts" + local _M = {} function _M.extend_table(defaults, values) @@ -17,4 +19,22 @@ function _M.is_array(t) return true end +local _const_mt = { + get = function(t, key, version) + if not version then version = CONSTS.MAX_PROTOCOL_VERSION end + + local const, version_consts + while version >= CONSTS.MIN_PROTOCOL_VERSION and const == nil do + version_consts = t[version] ~= nil and t[version] or t + const = rawget(version_consts, key) + version = version - 1 + end + return const + end +} + +_const_mt.__index = _const_mt + +_M.const_mt = _const_mt + return _M From fbf31630380f961c6d8453e32802f0f80eac79a8 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Wed, 4 Nov 2015 01:33:37 -0800 Subject: [PATCH 10/78] delete pre-rewrite files --- lua-cassandra-0.3.6-0.rockspec | 32 +- spec/auth.lua | 51 -- spec/cassandra_spec.lua | 9 - .../client_spec.lua | 0 spec/integration_spec.lua | 521 ------------------ spec/marshallers/marshall_v2_spec.lua | 116 ---- spec/marshallers/marshall_v3_spec.lua | 113 ---- spec/{rewrite_unit => unit}/buffer_spec.lua | 0 .../cql_types_buffer_spec.lua | 0 spec/{rewrite_unit => unit}/requests_spec.lua | 0 spec/{rewrite_unit => unit}/utils_spec.lua | 0 src/_cassandra.lua | 115 ---- src/cassandra.lua | 3 - .../authenticators/PasswordAuthenticator.lua | 43 -- src/cassandra/batch.lua | 24 - src/cassandra/constants/constants_v2.lua | 104 ---- src/cassandra/constants/constants_v3.lua | 15 - src/cassandra/error.lua | 33 -- src/cassandra/marshallers/marshall_v2.lua | 348 ------------ src/cassandra/marshallers/marshall_v3.lua | 123 ----- src/cassandra/marshallers/unmarshall_v2.lua | 231 -------- src/cassandra/marshallers/unmarshall_v3.lua | 124 ----- src/cassandra/protocol/reader_v2.lua | 208 ------- src/cassandra/protocol/reader_v3.lua | 77 --- src/cassandra/protocol/writer_v2.lua | 43 -- src/cassandra/protocol/writer_v3.lua | 14 - src/cassandra/session.lua | 303 ---------- src/cassandra/utils.lua | 112 ---- src/cassandra/v2.lua | 3 - 29 files changed, 3 insertions(+), 2762 deletions(-) delete mode 100644 spec/auth.lua delete mode 100644 spec/cassandra_spec.lua rename spec/{rewrite_integration => integration}/client_spec.lua (100%) delete mode 100644 spec/integration_spec.lua delete mode 100644 spec/marshallers/marshall_v2_spec.lua delete mode 100644 spec/marshallers/marshall_v3_spec.lua rename spec/{rewrite_unit => unit}/buffer_spec.lua (100%) rename spec/{rewrite_unit => unit}/cql_types_buffer_spec.lua (100%) rename spec/{rewrite_unit => unit}/requests_spec.lua (100%) rename spec/{rewrite_unit => unit}/utils_spec.lua (100%) delete mode 100644 src/_cassandra.lua delete mode 100644 src/cassandra.lua delete mode 100644 src/cassandra/authenticators/PasswordAuthenticator.lua delete mode 100644 src/cassandra/batch.lua delete mode 100644 src/cassandra/constants/constants_v2.lua delete mode 100644 src/cassandra/constants/constants_v3.lua delete mode 100644 src/cassandra/error.lua delete mode 100644 src/cassandra/marshallers/marshall_v2.lua delete mode 100644 src/cassandra/marshallers/marshall_v3.lua delete mode 100644 src/cassandra/marshallers/unmarshall_v2.lua delete mode 100644 src/cassandra/marshallers/unmarshall_v3.lua delete mode 100644 src/cassandra/protocol/reader_v2.lua delete mode 100644 src/cassandra/protocol/reader_v3.lua delete mode 100644 src/cassandra/protocol/writer_v2.lua delete mode 100644 src/cassandra/protocol/writer_v3.lua delete mode 100644 src/cassandra/session.lua delete mode 100644 src/cassandra/utils.lua delete mode 100644 src/cassandra/v2.lua diff --git a/lua-cassandra-0.3.6-0.rockspec b/lua-cassandra-0.3.6-0.rockspec index a7648d7..0803503 100644 --- a/lua-cassandra-0.3.6-0.rockspec +++ b/lua-cassandra-0.3.6-0.rockspec @@ -1,43 +1,17 @@ package = "lua-cassandra" -version = "0.3.6-0" +version = "0.4.0-0" source = { url = "git://github.com/thibaultCha/lua-cassandra", - tag = "0.3.6" + tag = "0.4.0" } description = { - summary = "Lua Cassandra client", + summary = "Lua Cassandra driver", homepage = "http://thibaultcha.github.io/lua-cassandra", license = "MIT" } -dependencies = { - "luabitop ~> 1.0.2-2" -} build = { type = "builtin", modules = { - ["cassandra"] = "src/cassandra.lua", - ["_cassandra"] = "src/_cassandra.lua", - ["cassandra.v2"] = "src/cassandra/v2.lua", - ["cassandra.batch"] = "src/cassandra/batch.lua", - ["cassandra.error"] = "src/cassandra/error.lua", - ["cassandra.session"] = "src/cassandra/session.lua", - - ["cassandra.constants.constants_v2"] = "src/cassandra/constants/constants_v2.lua", - ["cassandra.constants.constants_v3"] = "src/cassandra/constants/constants_v3.lua", - - ["cassandra.marshallers.marshall_v2"] = "src/cassandra/marshallers/marshall_v2.lua", - ["cassandra.marshallers.marshall_v3"] = "src/cassandra/marshallers/marshall_v3.lua", - ["cassandra.marshallers.unmarshall_v2"] = "src/cassandra/marshallers/unmarshall_v2.lua", - ["cassandra.marshallers.unmarshall_v3"] = "src/cassandra/marshallers/unmarshall_v3.lua", - - ["cassandra.protocol.reader_v2"] = "src/cassandra/protocol/reader_v2.lua", - ["cassandra.protocol.reader_v3"] = "src/cassandra/protocol/reader_v3.lua", - ["cassandra.protocol.writer_v2"] = "src/cassandra/protocol/writer_v2.lua", - ["cassandra.protocol.writer_v3"] = "src/cassandra/protocol/writer_v3.lua", - - ["cassandra.authenticators.PasswordAuthenticator"] = "src/cassandra/authenticators/PasswordAuthenticator.lua", - ["cassandra.utils"] = "src/cassandra/utils.lua", - ["cassandra.classic"] = "src/cassandra/classic.lua" } } diff --git a/spec/auth.lua b/spec/auth.lua deleted file mode 100644 index 5f7498e..0000000 --- a/spec/auth.lua +++ /dev/null @@ -1,51 +0,0 @@ --------- --- To run this test suite, edit your cassandra.yaml file and --- change the `authenticator` property to `PasswordAuthenticator`. --- `cassandra:cassandra` is the default root user. - -local cassandra = require "cassandra" -local PasswordAuthenticator = require "cassandra.authenticators.PasswordAuthenticator" - -describe("PasswordAuthenticator", function() - it("should instanciate a PasswordAuthenticator", function() - local authenticator = PasswordAuthenticator("cassandra", "cassandra") - assert.truthy(authenticator) - end) - it("should raise an error if missing a user or password", function() - assert.has_error(function() - PasswordAuthenticator() - end, "no user provided for PasswordAuthenticator") - - assert.has_error(function() - PasswordAuthenticator("cassandra") - end, "no password provided for PasswordAuthenticator") - end) - it("should authenticate against a cluster with PasswordAuthenticator", function() - local ok, res, err - local authenticator = PasswordAuthenticator("cassandra", "cassandra") - - local session = cassandra:new() - ok, err = session:connect("127.0.0.1", nil, authenticator) - assert.falsy(err) - assert.True(ok) - - res, err = session:execute("SELECT * FROM system_auth.users") - assert.falsy(err) - assert.truthy(res) - assert.equal("ROWS", res.type) - end) - it("should return an error if no credentials are provided", function() - local session = cassandra:new() - local ok, err = session:connect("127.0.0.1") - assert.False(ok) - assert.equal("cluster requires authentication, but no authenticator was given to the session", err) - end) - it("should return an error if credentials are incorrect", function() - local authenticator = PasswordAuthenticator("cassandra", "password") - - local session = cassandra:new() - local ok, err = session:connect("127.0.0.1", nil, authenticator) - assert.False(ok) - assert.equal("Cassandra returned error (Bad credentials): Username and/or password are incorrect", err.message) - end) -end) diff --git a/spec/cassandra_spec.lua b/spec/cassandra_spec.lua deleted file mode 100644 index 4a4850d..0000000 --- a/spec/cassandra_spec.lua +++ /dev/null @@ -1,9 +0,0 @@ -local cassandra = require "cassandra" - -describe("Cassandra", function() - it("should have type annotation shortands", function() - assert.has_no_error(function() - cassandra.uuid() - end) - end) -end) diff --git a/spec/rewrite_integration/client_spec.lua b/spec/integration/client_spec.lua similarity index 100% rename from spec/rewrite_integration/client_spec.lua rename to spec/integration/client_spec.lua diff --git a/spec/integration_spec.lua b/spec/integration_spec.lua deleted file mode 100644 index e95c104..0000000 --- a/spec/integration_spec.lua +++ /dev/null @@ -1,521 +0,0 @@ -local cassandra_v2 = require "cassandra.v2" -local cassandra_v3 = require "cassandra" - -local sess = cassandra_v3:new() -local ok, err = sess:connect({"127.0.0.1:9041"}) -local inspect = require "inspect" -print(inspect(err)) - - -describe("Session", function() -for _, cass in ipairs({{v = "v2", c = cassandra_v2}, { v = "v3", c = cassandra_v3}}) do -local cassandra = cass.c - -describe("Protocol #"..cass.v, function() - describe(":new()", function() - it("should instanciate a session", function() - local session = cassandra:new() - assert.truthy(session) - assert.truthy(session.socket) - end) - end) - - describe(":set_keepalive()", function() - it("should return an error if trying to use the cosocket API from luasocket", function() - local session = cassandra:new() - local err = select(2, session:set_keepalive()) - assert.equal("luasocket does not support reusable sockets", err.message) - end) - end) - - describe(":get_reused_times()", function() - it("should return an error if trying to use the cosocket API from luasocket", function() - local session = cassandra:new() - local err = select(2, session:get_reused_times()) - assert.equal("luasocket does not support reusable sockets", err.message) - end) - end) - - describe(":connect()", function() - it("should fail if no contact points are given", function() - local session = cassandra:new() - assert.has_error(function() - session:connect() - end, "no contact points provided") - end) - it("should connect if a contact point is given as a string", function() - local session = cassandra:new() - assert.has_no_error(function() - local ok, err = session:connect("127.0.0.1") - assert.falsy(err) - assert.True(ok) - end) - end) - it("should connect if some contact points are given as an array", function() - local session = cassandra:new() - assert.has_no_error(function() - local ok, err = session:connect({"localhost", "127.0.0.1"}) - assert.falsy(err) - assert.True(ok) - end) - end) - it("should try another host if others fail", function() - local session = cassandra:new() - local ok, err = session:connect({"0.0.0.1", "0.0.0.2", "0.0.0.3", "127.0.0.1"}) - assert.falsy(err) - assert.True(ok) - end) - it("should return error if it fails to connect to all hosts", function() - local session = cassandra:new() - local ok, err = session:connect({"0.0.0.1", "0.0.0.2", "0.0.0.3"}) - assert.False(ok) - assert.truthy(err) - end) - it("should connect to a given port", function() - local session = cassandra:new() - local ok, err = session:connect("127.0.0.1", 9042) - assert.falsy(err) - assert.True(ok) - end) - it("should accept overriding the port for some hosts", function() - -- If a contact point is of the form "host:port", this port will overwrite the one given as parameter of `connect`. - local session = cassandra:new() - local ok, err = session:connect({"127.0.0.1:9042"}, 9999) - assert.True(ok) - assert.falsy(err) - end) - end) - - describe(":close()", function() - local session = cassandra:new() - setup(function() - local ok = session:connect("127.0.0.1") - assert.True(ok) - end) - it("should close a connected session", function() - local closed, err = session:close() - assert.equal(1, closed) - assert.falsy(err) - end) - end) - - describe(":execute()", function() - local session = cassandra:new() - local row - setup(function() - local ok = session:connect("127.0.0.1") - assert.True(ok) - end) - teardown(function() - session:close() - end) - it("should execute a query", function() - local res, err = session:execute("SELECT cql_version, native_protocol_version, release_version FROM system.local") - assert.falsy(err) - assert.truthy(res) - assert.equal("ROWS", res.type) - assert.equal(1, #res) - row = res[1] - end) - describe("result rows", function() - it("should be accessible by index or column name", function() - if not row then pending() end - assert.equal(row[1], row.cql_version) - assert.equal(row[2], row.native_protocol_version) - assert.equal(row[3], row.release_version) - end) - end) - describe("errors", function() - it("should return a Cassandra error", function() - local res, err = session:execute("DESCRIBE") - assert.falsy(res) - assert.equal("Cassandra returned error (Syntax_error): line 1:0 no viable alternative at input 'DESCRIBE' ([DESCRIBE])", tostring(err)) - end) - end) - end) - - describe("Prepared Statements", function() - local session = cassandra:new() - setup(function() - local ok = session:connect("127.0.0.1") - assert.True(ok) - end) - teardown(function() - session:close() - end) - describe(":prepare()", function() - it("should prepare a query", function() - local stmt, err = session:prepare("SELECT native_protocol_version FROM system.local") - assert.falsy(err) - assert.truthy(stmt) - assert.equal("PREPARED", stmt.type) - assert.truthy(stmt.id) - end) - it("should prepare a query with tracing", function() - local stmt, err = session:prepare("SELECT native_protocol_version FROM system.local", true) - assert.falsy(err) - assert.truthy(stmt) - assert.equal("PREPARED", stmt.type) - assert.truthy(stmt.id) - assert.truthy(stmt.tracing_id) - end) - it("should prepare a query with binded parameters", function() - local stmt, err = session:prepare("SELECT * FROM system.local WHERE key = ?") - assert.falsy(err) - assert.truthy(stmt) - assert.equal("PREPARED", stmt.type) - assert.truthy(stmt.id) - end) - end) - end) - - describe("Functional use case", function() - local session = cassandra:new() - setup(function() - local ok = session:connect("127.0.0.1") - assert.True(ok) - local _, err = session:execute [[ - CREATE KEYSPACE IF NOT EXISTS lua_cassandra_tests - WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor': 1} - ]] - assert.falsy(err) - end) - teardown(function() - session:execute("DROP KEYSPACE lua_cassandra_tests") - session:close() - end) - describe(":set_keyspace()", function() - it("should set the session's keyspace", function() - local res, err = session:set_keyspace("lua_cassandra_tests") - assert.falsy(err) - assert.truthy(res) - assert.equal("SET_KEYSPACE", res.type) - assert.equal("lua_cassandra_tests", res.keyspace) - end) - end) - describe(":execute()", function() - setup(function() - local _, err = session:execute [[ - CREATE TABLE IF NOT EXISTS users( - id uuid, - name varchar, - age int, - PRIMARY KEY(id, age) - ) - ]] - assert.falsy(err) - end) - it("should execute with binded parameters", function() - local res, err = session:execute([[ INSERT INTO users(id, name, age) - VALUES(?, ?, ?) - ]], {cassandra.uuid("2644bada-852c-11e3-89fb-e0b9a54a6d93"), "Bob", 42}) - assert.falsy(err) - assert.truthy(res) - assert.equal("VOID", res.type) - end) - it("should execute a prepared statement", function() - local err, stmt, res - stmt, err = session:prepare("SELECT * FROM users") - assert.falsy(err) - assert.truthy(stmt) - - res, err = session:execute(stmt) - assert.falsy(err) - assert.truthy(res) - assert.equal("ROWS", res.type) - assert.equal(1, #res) - assert.equal("Bob", res[1].name) - assert.equal(42, res[1].age) - end) - it("should execute a prepared statement with binded parameters", function() - local err, stmt, res - stmt, err = session:prepare("SELECT * FROM users WHERE id = ?") - assert.falsy(err) - assert.truthy(stmt) - - res, err = session:execute(stmt, {cassandra.uuid("2644bada-852c-11e3-89fb-e0b9a54a6d93")}) - assert.falsy(err) - assert.truthy(res) - assert.equal("ROWS", res.type) - assert.equal(1, #res) - assert.equal("Bob", res[1].name) - assert.equal(42, res[1].age) - end) - describe("execute options", function() - it("should be possible to query with tracing", function() - local rows, err = session:execute("SELECT * FROM system.local", nil, {tracing = true}) - assert.falsy(err) - assert.truthy(rows.tracing_id) - end) - it("should support the serial_consitency flag", function() - -- serial_consistency only works for conditional update statements but - -- we are here tracking the driver's behaviour when passing the flag - local _, err = session:execute([[ - INSERT INTO users(id, age, name) VALUES(uuid(), 30, 'leo') IF NOT EXISTS - ]], nil, {serial_consistency = cassandra.consistency.LOCAL_SERIAL}) - assert.falsy(err) - end) - end) - describe("Pagination", function() - setup(function() - local err = select(2, session:execute("TRUNCATE users")) - assert.falsy(err) - for i = 1, 200 do - err = select(2, session:execute("INSERT INTO users(id, name, age) VALUES(uuid(), ?, ?)", - { "user"..i, i })) - if err then error(err) end - end - end) - it("should fetch everything given that the default page size is big enough", function() - local res, err = session:execute("SELECT * FROM users") - assert.falsy(err) - assert.equal(200, #res) - end) - it("should support a page_size option", function() - local rows, err = session:execute("SELECT * FROM users", nil, {page_size = 200}) - assert.falsy(err) - assert.same(200, #rows) - - rows, err = session:execute("SELECT * FROM users", nil, {page_size = 100}) - assert.falsy(err) - assert.same(100, #rows) - end) - it("should return metadata flags about pagination", function() - local res, err = session:execute("SELECT * FROM users", nil, {page_size = 100}) - assert.falsy(err) - assert.True(res.meta.has_more_pages) - assert.truthy(res.meta.paging_state) - - -- Full page - res, err = session:execute("SELECT * FROM users") - assert.falsy(err) - assert.False(res.meta.has_more_pages) - assert.falsy(res.meta.paging_state) - end) - it("should fetch the next page if given a `paging_state` option", function() - local res, err = session:execute("SELECT * FROM users", nil, {page_size = 100}) - assert.falsy(err) - assert.equal(100, #res) - - res, err = session:execute("SELECT * FROM users", nil, { - page_size = 100, - paging_state = res.meta.paging_state - }) - assert.falsy(err) - assert.equal(100, #res) - end) - describe("auto_paging", function() - it("should return an iterator if given an `auto_paging` options", function() - local page_tracker = 0 - for rows, err, page in session:execute("SELECT * FROM users", nil, {page_size = 10, auto_paging = true}) do - assert.falsy(err) - page_tracker = page_tracker + 1 - assert.equal(page_tracker, page) - assert.equal(10, #rows) - end - - assert.equal(20, page_tracker) - end) - it("should return the latest page of a set", function() - -- When the latest page contains only 1 element - local page_tracker = 0 - for rows, err, page in session:execute("SELECT * FROM users", nil, {page_size = 199, auto_paging = true}) do - assert.falsy(err) - page_tracker = page_tracker + 1 - assert.equal(page_tracker, page) - end - - assert.equal(2, page_tracker) - - -- Even if all results are fetched in the first page - page_tracker = 0 - for rows, err, page in session:execute("SELECT * FROM users", nil, {auto_paging = true}) do - assert.falsy(err) - page_tracker = page_tracker + 1 - assert.equal(page_tracker, page) - assert.equal(200, #rows) - end - - assert.same(1, page_tracker) - end) - it("should return any error", function() - -- This test validates the behaviour of err being returned if no - -- results are returned (most likely because of an invalid query) - local page_tracker = 0 - for rows, err, page in session:execute("SELECT * FROM users WHERE col = 500", nil, {auto_paging = true}) do - assert.truthy(err) -- 'col' is not a valid column - assert.equal(0, page) - page_tracker = page_tracker + 1 - end - - -- Assert the loop has been run once. - assert.equal(1, page_tracker) - end) - end) - end) - end) -- describe :execute() - describe("BatchStatement", function() - setup(function() - local err = select(2, session:execute("TRUNCATE users")) - assert.falsy(err) - end) - it("should instanciate a batch statement", function() - local batch = cassandra:BatchStatement() - assert.truthy(batch) - assert.equal("table", type(batch.queries)) - assert.True(batch.is_batch_statement) - end) - it("should instanciate a logged batch by default", function() - local batch = cassandra:BatchStatement() - assert.equal(cassandra.batch_types.LOGGED, batch.type) - end) - it("should instanciate different types of batch", function() - -- Unlogged - local batch = cassandra:BatchStatement(cassandra.batch_types.UNLOGGED) - assert.equal(cassandra.batch_types.UNLOGGED, batch.type) - -- Counter - batch = cassandra:BatchStatement(cassandra.batch_types.COUNTER) - assert.equal(cassandra.batch_types.COUNTER, batch.type) - end) - it("should be possible to add queries to a batch", function() - local batch = cassandra:BatchStatement() - assert.has_no_error(function() - batch:add("INSERT INTO users(id, name) VALUES(uuid(), ?)", {"Laura"}) - batch:add("INSERT INTO users(id, name) VALUES(uuid(), ?)", {"James"}) - end) - assert.equal(2, #batch.queries) - end) - it("should be possible to execute a batch", function() - local batch = cassandra:BatchStatement() - batch:add("INSERT INTO users(id, age, name) VALUES(uuid(), ?, ?)", {21, "Laura"}) - batch:add("INSERT INTO users(id, age, name) VALUES(uuid(), ?, ?)", {22, "James"}) - - local res, err = session:execute(batch) - assert.falsy(err) - assert.truthy(res) - assert.equal("VOID", res.type) - - -- Check insertion - res, err = session:execute("SELECT * FROM users") - assert.falsy(err) - assert.equal(2, #res) - end) - it("should execute unlogged batch statement", function() - local batch = cassandra:BatchStatement(cassandra.batch_types.UNLOGGED) - batch:add("INSERT INTO users(id, age, name) VALUES(uuid(), ?, ?)", {21, "Laura"}) - batch:add("INSERT INTO users(id, age, name) VALUES(uuid(), ?, ?)", {22, "James"}) - - local res, err = session:execute(batch) - assert.falsy(err) - assert.truthy(res) - assert.equal("VOID", res.type) - - -- Check insertion - res, err = session:execute("SELECT * FROM users") - assert.falsy(err) - assert.equal(4, #res) - end) - describe("Counter batch", function() - setup(function() - local err = select(2, session:execute([[ - CREATE TABLE IF NOT EXISTS counter_test_table( - key text PRIMARY KEY, - value counter - ) - ]])) - assert.falsy(err) - end) - it("should execute counter batch statement", function() - local batch = cassandra:BatchStatement(cassandra.batch_types.COUNTER) - - -- Query - batch:add("UPDATE counter_test_table SET value = value + 1 WHERE key = 'key'") - - -- Binded queries - batch:add("UPDATE counter_test_table SET value = value + 1 WHERE key = ?", {"key"}) - batch:add("UPDATE counter_test_table SET value = value + 1 WHERE key = ?", {"key"}) - - -- Prepared statement - local stmt, res, err - stmt, err = session:prepare [[ - UPDATE counter_test_table SET value = value + 1 WHERE key = ? - ]] - assert.falsy(err) - batch:add(stmt, {"key"}) - - res, err = session:execute(batch) - assert.falsy(err) - assert.truthy(res) - - res, err = session:execute [[ - SELECT value from counter_test_table WHERE key = 'key' - ]] - assert.falsy(err) - assert.equal(4, res[1].value) - end) - end) - end) - end) -- describe Functional Use Case -end) -- describe Protocol -end -end) -- describe Session - -describe("Only v3", function() - local session = cassandra_v3:new() - setup(function() - local ok = session:connect("127.0.0.1") - assert.True(ok) - local _, err = session:execute [[ - CREATE KEYSPACE IF NOT EXISTS lua_cassandra_tests - WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor': 2} - ]] - assert.falsy(err) - session:set_keyspace("lua_cassandra_tests") - end) - teardown(function() - session:execute("DROP KEYSPACE lua_cassandra_tests") - session:close() - end) - describe("User Defined Type", function() - setup(function() - local err = select(2, session:execute([[ - CREATE TYPE address ( - street text, - city text, - zip int, - country text - ) - ]])) - assert.falsy(err) - - err = select(2, session:execute([[ - CREATE TABLE user_profiles ( - email text PRIMARY KEY, - address frozen
- ) - ]])) - assert.falsy(err) - end) - teardown(function() - session:execute("DROP TYPE address") - session:execute("DROP TABLE user_profiles") - end) - it("should be possible to insert and get value back", function() - local rows, err - err = select(2, session:execute([[ - INSERT INTO user_profiles(email, address) VALUES (?, ?) - ]], {"email@domain.com", cassandra_v3.udt({ "montgomery street", "san francisco", 94111, nil })})) - - assert.falsy(err) - - rows, err = session:execute("SELECT address FROM user_profiles WHERE email = 'email@domain.com'") - assert.falsy(err) - assert.same(1, #rows) - local row = rows[1] - assert.same("montgomery street", row.address.street) - assert.same("san francisco", row.address.city) - assert.same(94111, row.address.zip) - assert.same("", row.address.country) - end) - end) -end) diff --git a/spec/marshallers/marshall_v2_spec.lua b/spec/marshallers/marshall_v2_spec.lua deleted file mode 100644 index 3b98df5..0000000 --- a/spec/marshallers/marshall_v2_spec.lua +++ /dev/null @@ -1,116 +0,0 @@ -local Marshall_v2 = require "cassandra.marshallers.marshall_v2" -local Unsmarshall_v2 = require "cassandra.marshallers.unmarshall_v2" - -local marshall_v2 = Marshall_v2() -local unsmarshall_v2 = Unsmarshall_v2() - -describe("Marshallers v2", function() - - local fixtures = { - -- custom - ascii = {"ascii"}, - bigint = {0, 42, -42, 42000000000, -42000000000}, - blob = {"\005\042", string.rep("blob", 10000)}, - boolean = {true, false}, - --counter - -- decimal - double = {0, 1.0000000000000004, -1.0000000000000004}, - float = {0, 3.14151, -3.14151}, - int = {0, 4200, -42}, - text = {"some text"}, - timestamp = {1405356926}, - uuid = {"1144bada-852c-11e3-89fb-e0b9a54a6d11"}, - varchar = {"string"}, - varint = {0, 4200, -42}, - timeuuid = {"1144bada-852c-11e3-89fb-e0b9a54a6d11"}, - inet = {["127.0.0.1"] = "127.0.0.1", - ["2001:0db8:85a3:0042:1000:8a2e:0370:7334"] = "2001:0db8:85a3:0042:1000:8a2e:0370:7334", - ["2001:0db8:0000:0000:0000:0000:0000:0001"] = "2001:db8::1", - ["2001:0db8:85a3:0000:0000:0000:0000:0010"] = "2001:db8:85a3::10", - ["2001:0db8:85a3:0000:0000:0000:0000:0100"] = "2001:db8:85a3::100", - ["0000:0000:0000:0000:0000:0000:0000:0001"] = "::1", - ["0000:0000:0000:0000:0000:0000:0000:0000"] = "::"} - } - - for fix_type, fix_values in pairs(fixtures) do - it("should encode and decode a ["..fix_type.."]", function() - for expected, fix_value in pairs(fix_values) do - local encoded = marshall_v2:value_representation(fix_value, marshall_v2.TYPES[fix_type]) - local buffer = unsmarshall_v2:create_buffer(encoded) - local decoded = unsmarshall_v2:read_value(buffer, { id = marshall_v2.TYPES[fix_type] }) - - if fix_type == "float" then - local delta = 0.0000001 - assert.True(math.abs(decoded - fix_value) < delta) - elseif fix_type == "inet" then - assert.equal(expected, decoded) - else - assert.equal(fix_value, decoded) - end - end - end) - end - - it("should encode and decode a [list]", function() - local list_fixtures = { - {value_type = "text", value = {"abc", "def"}}, - {value_type = "int", value = {0, 1, 2, 42, -42}}, - } - - for _, fixture in ipairs(list_fixtures) do - local encoded = marshall_v2:value_representation(fixture.value, marshall_v2.TYPES.list) - local buffer = unsmarshall_v2:create_buffer(encoded) - - local value_type = { id = marshall_v2.TYPES[fixture.value_type] } - - local decoded = unsmarshall_v2:read_value(buffer, { - id = marshall_v2.TYPES.list, - value = value_type - }) - assert.same(fixture.value, decoded) - end - end) - - it("should encode and decode a [map]", function() - local map_fixtures = { - {key_type = "text", value_type = "text", value = {k1='v1', k2='v2'}}, - {key_type = "text", value_type = "int", value = {k1=1, k2=2}}, - {key_type = "text", value_type = "int", value = {}}, - } - - for _, fixture in ipairs(map_fixtures) do - local encoded = marshall_v2:value_representation(fixture.value, marshall_v2.TYPES.map) - local buffer = unsmarshall_v2:create_buffer(encoded) - - local key_type = {id = marshall_v2.TYPES[fixture.key_type]} - local value_type = {id = marshall_v2.TYPES[fixture.value_type]} - - local decoded = unsmarshall_v2:read_value(buffer, { - id = marshall_v2.TYPES.map, - value = {key_type, value_type} - }) - assert.same(fixture.value, decoded) - end - end) - - it("should encode and decode a [set]", function() - local set_fixtures = { - {value_type = "text", value = {"abc", "def"}}, - {value_type = "int", value = {0, 1, 2, 42, -42}}, - } - - for _, fixture in ipairs(set_fixtures) do - local encoded = marshall_v2:value_representation(fixture.value, marshall_v2.TYPES.set) - local buffer = unsmarshall_v2:create_buffer(encoded) - - local value_type = {id = marshall_v2.TYPES[fixture.value_type]} - - local decoded = unsmarshall_v2:read_value(buffer, { - id = marshall_v2.TYPES.set, - value = value_type - }) - assert.same(fixture.value, decoded) - end - end) - -end) diff --git a/spec/marshallers/marshall_v3_spec.lua b/spec/marshallers/marshall_v3_spec.lua deleted file mode 100644 index e32affe..0000000 --- a/spec/marshallers/marshall_v3_spec.lua +++ /dev/null @@ -1,113 +0,0 @@ -local Marshall_v3 = require "cassandra.marshallers.marshall_v3" -local Unsmarshall_v3 = require "cassandra.marshallers.unmarshall_v3" - -local marshall_v3 = Marshall_v3() -local unsmarshall_v3 = Unsmarshall_v3() - -describe("Marshallers v3", function() - - it("should encode and decode a [tuple]", function() - local fixtures = { - {types = {"text", "int", "float"}, values = {"foo", 1, 3.14151}}, - {types = {"text", "int"}, values = {"foo", 1}}, - {types = {"text", "text", "int"}, values = {"foo", "abcd", 123}} - } - - for _, fixture in ipairs(fixtures) do - local tuple_type = { - id = marshall_v3.TYPES.tuple, - fields = {} - } - for _, part_type in ipairs(fixture.types) do - table.insert(tuple_type.fields, {type = { id = marshall_v3.TYPES[part_type] }}) - end - - local encoded = marshall_v3:value_representation(fixture.values, marshall_v3.TYPES.tuple) - local buffer = unsmarshall_v3:create_buffer(encoded) - local decoded = unsmarshall_v3:read_value(buffer, tuple_type) - - for i, v in ipairs(decoded) do - if fixture.types[i] == "float" then - local delta = 0.0000001 - assert.True(math.abs(v - fixture.values[i]) < delta) - else - assert.equal(fixture.values[i], v) - end - end - end - end) - - it("should encode and decode a [udt]", function() - local fixtures = { - {types = {some_text="text", some_int="int", some_float="float"}, values = {"foo", 1, 3.14151}}, - {types = {some_text="text", some_int="int"}, values = {"foo", 1}} - } - - for _, fixture in ipairs(fixtures) do - local udt_type = { - id = marshall_v3.TYPES.udt, - fields = {} - } - for part_name, part_type in pairs(fixture.types) do - table.insert(udt_type.fields, { type = {id=marshall_v3.TYPES[part_type]}, - name = part_name }) - end - - local encoded = marshall_v3:value_representation(fixture.values, marshall_v3.TYPES.udt) - local buffer = unsmarshall_v3:create_buffer(encoded) - local decoded = unsmarshall_v3:read_value(buffer, udt_type) - - for i, v in ipairs(decoded) do - if fixture.types[i] == "float" then - local delta = 0.0000001 - assert.True(math.abs(v - fixture.values[i]) < delta) - else - assert.equal(fixture.values[i], v) - end - end - end - end) - - it("should encode and decode a [list]", function() - local fixtures = { - {value_type = "text", value = {"abc", "def"}}, - {value_type = "int", value = {0, 1, 2, 42, -42}}, - } - - for _, fixture in ipairs(fixtures) do - local encoded = marshall_v3:value_representation(fixture.value, marshall_v3.TYPES.list) - local buffer = unsmarshall_v3:create_buffer(encoded) - - local value_type = {id = marshall_v3.TYPES[fixture.value_type]} - - local decoded = unsmarshall_v3:read_value(buffer, { - id = marshall_v3.TYPES.list, - value = value_type - }) - assert.same(fixture.value, decoded) - end - end) - - it("should encode and decode a [map]", function() - local fixtures = { - { key_type = "text", value_type = "text", value = {k1='v1', k2='v2'} }, - { key_type = "text", value_type = "int", value = {k1=1, k2=2} }, - { key_type = "text", value_type = "int", value = {} }, - } - - for _, fixture in ipairs(fixtures) do - local encoded = marshall_v3:value_representation(fixture.value, marshall_v3.TYPES.map) - local buffer = unsmarshall_v3:create_buffer(encoded) - - local key_type = {id = marshall_v3.TYPES[fixture.key_type]} - local value_type = {id = marshall_v3.TYPES[fixture.value_type]} - - local decoded = unsmarshall_v3:read_value(buffer, { - id = marshall_v3.TYPES.map, - value = {key_type, value_type} - }) - assert.same(fixture.value, decoded) - end - end) - -end) diff --git a/spec/rewrite_unit/buffer_spec.lua b/spec/unit/buffer_spec.lua similarity index 100% rename from spec/rewrite_unit/buffer_spec.lua rename to spec/unit/buffer_spec.lua diff --git a/spec/rewrite_unit/cql_types_buffer_spec.lua b/spec/unit/cql_types_buffer_spec.lua similarity index 100% rename from spec/rewrite_unit/cql_types_buffer_spec.lua rename to spec/unit/cql_types_buffer_spec.lua diff --git a/spec/rewrite_unit/requests_spec.lua b/spec/unit/requests_spec.lua similarity index 100% rename from spec/rewrite_unit/requests_spec.lua rename to spec/unit/requests_spec.lua diff --git a/spec/rewrite_unit/utils_spec.lua b/spec/unit/utils_spec.lua similarity index 100% rename from spec/rewrite_unit/utils_spec.lua rename to spec/unit/utils_spec.lua diff --git a/src/_cassandra.lua b/src/_cassandra.lua deleted file mode 100644 index f8026fa..0000000 --- a/src/_cassandra.lua +++ /dev/null @@ -1,115 +0,0 @@ --------- --- This module allows the creation of sessions and provides shorthand --- annotations for type encoding and batch statement creation. --- Depending on how it will be initialized, it supports either the binary --- protocol v2 or v3: --- --- require "cassandra" -- binary protocol v3 (Cassandra 2.0.x and 2.1.x) --- require "cassandra.v2" -- binary procotol v2 (Cassandra 2.0.x) --- --- Shorthands to give a type to a value in a query: --- --- session:execute("SELECT * FROM users WHERE id = ?", { --- cassandra.uuid("2644bada-852c-11e3-89fb-e0b9a54a6d93") --- }) --- --- @module Cassandra - -local session = require "cassandra.session" -local batch_mt = require "cassandra.batch" - -local _M = {} - -function _M:__call(protocol) - local constants = require("cassandra.constants.constants_"..protocol) - local Marshaller = require("cassandra.marshallers.marshall_"..protocol) - local Unmarshaller = require("cassandra.marshallers.unmarshall_"..protocol) - local Writer = require("cassandra.protocol.writer_"..protocol) - local Reader = require("cassandra.protocol.reader_"..protocol) - - local marshaller = Marshaller(constants) - local unmarshaller = Unmarshaller() - local writer = Writer(marshaller, constants) - local reader = Reader(unmarshaller, constants) - - local cassandra_t = { - protocol = protocol, - writer = writer, - reader = reader, - constants = constants, - marshaller = marshaller, - unmarshaller = unmarshaller, - -- extern - consistency = constants.consistency, - batch_types = constants.batch_types - } - - return setmetatable(cassandra_t, _M) -end - --- Shorthand to create type annotations --- Ex: --- session:execute("...", {cassandra.uuid(some_uuid_str)}) -function _M:__index(key) - if self.marshaller.TYPES[key] then - return function(value) - return {type = key, value = value} - end - end - - return _M[key] -end - ---- Instanciate a new `Session`. --- Create a socket with the cosocket API if in Nginx and available, fallback to luasocket otherwise. --- The instanciated session will communicate using the binary protocol of the current cassandra --- implementation being required. --- @return session The created session. --- @return err Any `Error` encountered during the socket creation. -function _M:new() - local tcp, socket_type - if ngx and ngx.get_phase ~= nil and ngx.get_phase() ~= "init" then - -- openresty - tcp = ngx.socket.tcp - socket_type = "ngx" - else - -- fallback to luasocket - -- It's also a fallback for openresty in the - -- "init" phase that doesn't support Cosockets - tcp = require("socket").tcp - socket_type = "luasocket" - end - - local socket, err = tcp() - if not socket then - return nil, err - end - - local session_t = { - socket = socket, - socket_type = socket_type, - writer = self.writer, - reader = self.reader, - constants = self.constants, - marshaller = self.marshaller, - unmarshaller = self.unmarshaller - } - - return setmetatable(session_t, session) -end - ---- Instanciate a `BatchStatement`. --- The instanciated batch will then provide an ":add()" method to add queries, --- and can be executed by a session's ":execute()" function. --- See the related `BatchStatement` module and `batch.lua` example. --- See http://docs.datastax.com/en/cql/3.1/cql/cql_reference/batch_r.html --- @param batch_type The type of this batch. Can be one of: 'Logged, Unlogged, Counter' -function _M:BatchStatement(batch_type) - if not batch_type then - batch_type = self.constants.batch_types.LOGGED - end - - return setmetatable({type = batch_type, queries = {}}, batch_mt) -end - -return setmetatable({}, _M) diff --git a/src/cassandra.lua b/src/cassandra.lua deleted file mode 100644 index f15f616..0000000 --- a/src/cassandra.lua +++ /dev/null @@ -1,3 +0,0 @@ -local _cassandra = require "_cassandra" - -return _cassandra("v3") diff --git a/src/cassandra/authenticators/PasswordAuthenticator.lua b/src/cassandra/authenticators/PasswordAuthenticator.lua deleted file mode 100644 index b86780d..0000000 --- a/src/cassandra/authenticators/PasswordAuthenticator.lua +++ /dev/null @@ -1,43 +0,0 @@ ---- The client authenticator for the Cassandra `PasswordAuthenticator` IAuthenticator. --- To be instanciated with a user/password couple and given to a `Session` when --- connecting it. See the related `authentication.lua` example. --- @see http://docs.datastax.com/en/cassandra/1.2/cassandra/security/security_config_native_authenticate_t.html --- @usage local auth = PasswordAuthenticator("user", "password") --- @module PasswordAuthenticator - -local Object = require "cassandra.classic" -local marshaller = require "cassandra.marshallers.marshall_v2" - -local _M = Object:extend() - -function _M:new(user, password) - if user == nil then - error("no user provided for PasswordAuthenticator") - elseif password == nil then - error("no password provided for PasswordAuthenticator") - end - - self.user = user - self.password = password -end - -function _M:build_body() - return marshaller:bytes_representation(string.format("\0%s\0%s", self.user, self.password)) -end - -function _M:authenticate(session) - local frame_body = self:build_body() - local response, socket_err = session:send_frame_and_get_response(session.constants.op_codes.AUTH_RESPONSE, frame_body) - if socket_err then - return false, socket_err - end - - if response.op_code == session.constants.op_codes.AUTH_SUCCESS then - return true - else - local parsed_error = session.reader:read_error(response.buffer) - return false, parsed_error - end -end - -return _M diff --git a/src/cassandra/batch.lua b/src/cassandra/batch.lua deleted file mode 100644 index 30d5903..0000000 --- a/src/cassandra/batch.lua +++ /dev/null @@ -1,24 +0,0 @@ --------- --- This module represents a Cassandra batch statement. --- A batch can combine multiple data modification statements (INSERT, UPDATE, DELETE) --- into a single operation. --- A batch is instanciated by the `Cassandra` module. --- See the related `batch.lua` example. --- @see http://docs.datastax.com/en/cql/3.1/cql/cql_reference/batch_r.html --- @module BatchStatement - -local batch = {} - -batch.__index = batch - -batch.is_batch_statement = true - ---- Add a query to the batch operation. --- The query can either be a plain string or a prepared statement. --- @param query The query or prepared statement to add to the batch. --- @param args Arguments of the query. -function batch:add(query, args) - table.insert(self.queries, {query = query, args = args}) -end - -return batch diff --git a/src/cassandra/constants/constants_v2.lua b/src/cassandra/constants/constants_v2.lua deleted file mode 100644 index 477b069..0000000 --- a/src/cassandra/constants/constants_v2.lua +++ /dev/null @@ -1,104 +0,0 @@ -local error_codes = { - SERVER = 0x0000, - PROTOCOL = 0x000A, - BAD_CREDENTIALS = 0x0100, - UNAVAILABLE_EXCEPTION = 0x1000, - OVERLOADED = 0x1001, - IS_BOOTSTRAPPING = 0x1002, - TRUNCATE_ERROR = 0x1003, - WRITE_TIMEOUT = 0x1100, - READ_TIMEOUT = 0x1200, - SYNTAX_ERROR = 0x2000, - UNAUTHORIZED = 0x2100, - INVALID = 0x2200, - CONFIG_ERROR = 0x2300, - ALREADY_EXISTS = 0x2400, - UNPREPARED = 0x2500 -} - -return { - version_codes = { - REQUEST=0x02, - RESPONSE=0x82 - }, - flags = { - COMPRESSION=0x01, -- not implemented - TRACING=0x02 - }, - op_codes = { - ERROR=0x00, - STARTUP=0x01, - READY=0x02, - AUTHENTICATE=0x03, - -- 0x04 - OPTIONS=0x05, - SUPPORTED=0x06, - QUERY=0x07, - RESULT=0x08, - PREPARE=0x09, - EXECUTE=0x0A, - REGISTER=0x0B, - EVENT=0x0C, - BATCH=0x0D, - AUTH_CHALLENGE=0x0E, - AUTH_RESPONSE=0x0F, - AUTH_SUCCESS=0x10, - }, - batch_types = { - LOGGED=0, - UNLOGGED=1, - COUNTER=2 - }, - query_flags = { - VALUES=0x01, - SKIP_METADATA=0x02, -- not implemented - PAGE_SIZE=0x04, - PAGING_STATE=0x08, - -- 0x09 - SERIAL_CONSISTENCY=0x10 - }, - consistency = { - ANY=0x0000, - ONE=0x0001, - TWO=0x0002, - THREE=0x0003, - QUORUM=0x0004, - ALL=0x0005, - LOCAL_QUORUM=0x0006, - EACH_QUORUM=0x0007, - SERIAL=0x0008, - LOCAL_SERIAL=0x0009, - LOCAL_ONE=0x000A - }, - result_kinds = { - VOID=0x01, - ROWS=0x02, - SET_KEYSPACE=0x03, - PREPARED=0x04, - SCHEMA_CHANGE=0x05 - }, - rows_flags = { - GLOBAL_TABLES_SPEC=0x01, - HAS_MORE_PAGES=0x02, - -- 0x03 - NO_METADATA=0x04 - }, - error_codes = error_codes, - error_codes_translation = { - [error_codes.SERVER]="Server error", - [error_codes.PROTOCOL]="Protocol error", - [error_codes.BAD_CREDENTIALS]="Bad credentials", - [error_codes.UNAVAILABLE_EXCEPTION]="Unavailable exception", - [error_codes.OVERLOADED]="Overloaded", - [error_codes.IS_BOOTSTRAPPING]="Is_bootstrapping", - [error_codes.TRUNCATE_ERROR]="Truncate_error", - [error_codes.WRITE_TIMEOUT]="Write_timeout", - [error_codes.READ_TIMEOUT]="Read_timeout", - [error_codes.SYNTAX_ERROR]="Syntax_error", - [error_codes.UNAUTHORIZED]="Unauthorized", - [error_codes.INVALID]="Invalid", - [error_codes.CONFIG_ERROR]="Config_error", - [error_codes.ALREADY_EXISTS]="Already_exists", - [error_codes.UNPREPARED]="Unprepared" - }, -} diff --git a/src/cassandra/constants/constants_v3.lua b/src/cassandra/constants/constants_v3.lua deleted file mode 100644 index 2550e21..0000000 --- a/src/cassandra/constants/constants_v3.lua +++ /dev/null @@ -1,15 +0,0 @@ -local utils = require "cassandra.utils" -local constants_v2 = require "cassandra.constants.constants_v2" - -local constants_v3 = utils.deep_copy(constants_v2) - -constants_v3.version_codes.REQUEST = 0x03 -constants_v3.version_codes.RESPONSE = 0x83 - -constants_v3.query_flags.DEFAULT_TIMESTAMP = 0x20 -constants_v3.query_flags.NAMED_VALUES = 0x40 - -constants_v3.rows_flags.udt = 0x30 -constants_v3.rows_flags.tuple = 0x31 - -return constants_v3 diff --git a/src/cassandra/error.lua b/src/cassandra/error.lua deleted file mode 100644 index b2a63d2..0000000 --- a/src/cassandra/error.lua +++ /dev/null @@ -1,33 +0,0 @@ --------- --- Every error is represented by a table with the following properties. --- @module Error - ---- A description of any error returned by the library. --- @field code The error code for the error. `-1` means the error comes from the client. --- @field message A formatted error message (with the translated error code if is a Cassandra error). --- @field raw_message The raw error message as returned by Cassandra. --- @table error - -local error_mt = {} - -error_mt = { - __tostring = function(self) - return self.message - end, - __concat = function(a, b) - if getmetatable(a) == error_mt then - return a.message..b - else - return a..b.message - end - end, - __call = function(self, message, raw_message, code) - return setmetatable({ - code = code or -1, - message = message, - raw_message = raw_message or message - }, error_mt) - end -} - -return setmetatable({}, error_mt) diff --git a/src/cassandra/marshallers/marshall_v2.lua b/src/cassandra/marshallers/marshall_v2.lua deleted file mode 100644 index b502540..0000000 --- a/src/cassandra/marshallers/marshall_v2.lua +++ /dev/null @@ -1,348 +0,0 @@ -local utils = require "cassandra.utils" -local Object = require "cassandra.classic" -local constants = require "cassandra.constants.constants_v2" -local big_endian_representation = utils.big_endian_representation - -local _M = Object:extend() - -_M.TYPES = { - custom = 0x00, - ascii = 0x01, - bigint = 0x02, - blob = 0x03, - boolean = 0x04, - counter = 0x05, - decimal = 0x06, - double = 0x07, - float = 0x08, - int = 0x09, - text = 0x0A, - timestamp = 0x0B, - uuid = 0x0C, - varchar = 0x0D, - varint = 0x0E, - timeuuid = 0x0F, - inet = 0x10, - list = 0x20, - map = 0x21, - set = 0x22 -} - -function _M:new() -end - -function _M:identity_representation(value) - return value -end - -function _M:int_representation(num) - return big_endian_representation(num, 4) -end - -function _M:short_representation(num) - return big_endian_representation(num, 2) -end - -function _M:string_representation(str) - return self:short_representation(#str)..str -end - -function _M:long_string_representation(str) - return self:int_representation(#str)..str -end - -function _M:bytes_representation(bytes) - return self:int_representation(#bytes)..bytes -end - -function _M:short_bytes_representation(bytes) - return self:short_representation(#bytes)..bytes -end - -function _M:boolean_representation(value) - return value and "\001" or "\000" -end - -function _M:bigint_representation(n) - local first_byte = n >= 0 and 0 or 0xFF - return string.char(first_byte, -- only 53 bits from double - math.floor(n / 0x1000000000000) % 0x100, - math.floor(n / 0x10000000000) % 0x100, - math.floor(n / 0x100000000) % 0x100, - math.floor(n / 0x1000000) % 0x100, - math.floor(n / 0x10000) % 0x100, - math.floor(n / 0x100) % 0x100, - n % 0x100) -end - -function _M:uuid_representation(value) - local str = string.gsub(value, "-", "") - local buffer = {} - for i = 1, #str, 2 do - local byte_str = string.sub(str, i, i + 1) - buffer[#buffer + 1] = string.char(tonumber(byte_str, 16)) - end - return table.concat(buffer) -end - --- "inspired" by https://github.com/fperrad/lua-MessagePack/blob/master/src/MessagePack.lua -function _M:double_representation(number) - local sign = 0 - if number < 0.0 then - sign = 0x80 - number = -number - end - local mantissa, exponent = math.frexp(number) - if mantissa ~= mantissa then - return string.char(0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) -- nan - elseif mantissa == math.huge then - if sign == 0 then - return string.char(0x7F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) -- +inf - else - return string.char(0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) -- -inf - end - elseif mantissa == 0.0 and exponent == 0 then - return string.char(sign, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) -- zero - else - exponent = exponent + 0x3FE - mantissa = (mantissa * 2.0 - 1.0) * math.ldexp(0.5, 53) - return string.char(sign + math.floor(exponent / 0x10), - (exponent % 0x10) * 0x10 + math.floor(mantissa / 0x1000000000000), - math.floor(mantissa / 0x10000000000) % 0x100, - math.floor(mantissa / 0x100000000) % 0x100, - math.floor(mantissa / 0x1000000) % 0x100, - math.floor(mantissa / 0x10000) % 0x100, - math.floor(mantissa / 0x100) % 0x100, - mantissa % 0x100) - end -end - -function _M:float_representation(number) - if number == 0 then - return string.char(0x00, 0x00, 0x00, 0x00) - elseif number ~= number then - return string.char(0xFF, 0xFF, 0xFF, 0xFF) - else - local sign = 0x00 - if number < 0 then - sign = 0x80 - number = -number - end - local mantissa, exponent = math.frexp(number) - exponent = exponent + 0x7F - if exponent <= 0 then - mantissa = math.ldexp(mantissa, exponent - 1) - exponent = 0 - elseif exponent > 0 then - if exponent >= 0xFF then - return string.char(sign + 0x7F, 0x80, 0x00, 0x00) - elseif exponent == 1 then - exponent = 0 - else - mantissa = mantissa * 2 - 1 - exponent = exponent - 1 - end - end - mantissa = math.floor(math.ldexp(mantissa, 23) + 0.5) - return string.char(sign + math.floor(exponent / 2), - (exponent % 2) * 0x80 + math.floor(mantissa / 0x10000), - math.floor(mantissa / 0x100) % 0x100, - mantissa % 0x100) - end -end - -function _M:inet_representation(value) - local digits = {} - local hexadectets = {} - local ip = value:lower():gsub("::",":0000:") - if value:match(":") then - -- ipv6 - for hdt in string.gmatch(ip, "[%x]+") do - hexadectets[#hexadectets+1] = string.rep("0", 4 - #hdt) .. hdt - end - for idx, d in ipairs(hexadectets) do - while d == "0000" and 8 > #hexadectets do - table.insert(hexadectets, idx + 1, "0000") - end - for i = 1, 4, 2 do - digits[#digits + 1] = string.char(tonumber(string.sub(d, i, i + 1), 16)) - end - end - else - -- ipv4 - for d in string.gmatch(value, "(%d+)") do - table.insert(digits, string.char(d)) - end - end - return table.concat(digits) -end - -function _M:list_representation(elements) - local buffer = {self:short_representation(#elements)} - for _, value in ipairs(elements) do - buffer[#buffer + 1] = self:value_representation(value, nil, true) - end - return table.concat(buffer) -end - -function _M:set_representation(elements) - return self:list_representation(elements) -end - -function _M:map_representation(map) - local buffer = {} - local size = 0 - for key, value in pairs(map) do - buffer[#buffer + 1] = self:value_representation(key, nil, true) - buffer[#buffer + 1] = self:value_representation(value, nil, true) - size = size + 1 - end - return self:short_representation(size)..table.concat(buffer) -end - -function _M:string_map_representation(map) - local buffer = {} - local n = 0 - for k, v in pairs(map) do - buffer[#buffer + 1] = self:string_representation(k) - buffer[#buffer + 1] = self:string_representation(v) - n = n + 1 - end - return self:short_representation(n)..table.concat(buffer) -end - -_M.encoders = { - -- custom=0x00, - [_M.TYPES.ascii] = _M.identity_representation, - [_M.TYPES.bigint] = _M.bigint_representation, - [_M.TYPES.blob] = _M.identity_representation, - [_M.TYPES.boolean] = _M.boolean_representation, - [_M.TYPES.counter] = _M.bigint_representation, - -- decimal=0x06, - [_M.TYPES.double] = _M.double_representation, - [_M.TYPES.float] = _M.float_representation, - [_M.TYPES.int] = _M.int_representation, - [_M.TYPES.text] = _M.identity_representation, - [_M.TYPES.timestamp] = _M.bigint_representation, - [_M.TYPES.uuid] = _M.uuid_representation, - [_M.TYPES.varchar] = _M.identity_representation, - [_M.TYPES.varint] = _M.int_representation, - [_M.TYPES.timeuuid] = _M.uuid_representation, - [_M.TYPES.inet] = _M.inet_representation, - [_M.TYPES.list] = _M.list_representation, - [_M.TYPES.map] = _M.map_representation, - [_M.TYPES.set] = _M.set_representation -} - -function _M:value_representation(value, cass_type, short) - local infered_type - local value_lua_type = type(value) - if cass_type then - infered_type = cass_type - elseif value_lua_type == "number" and math.floor(value) == value then - infered_type = _M.TYPES.int - elseif value_lua_type == "number" then - infered_type = _M.TYPES.float - elseif value_lua_type == "boolean" then - infered_type = _M.TYPES.boolean - elseif value_lua_type == "table" and value.type == "null" then - if short then - infered_type = self:short_representation(-1) - else - infered_type = self:int_representation(-1) - end - elseif value_lua_type == "table" and value.type and value.value then - -- Value passed as a binded parameter. - infered_type = _M.TYPES[value.type] - value = value.value - else - infered_type = _M.TYPES.varchar - end - - local representation = _M.encoders[infered_type](self, value) - - if short then - return self:short_bytes_representation(representation) - else - return self:bytes_representation(representation) - end -end - -function _M:values_representation(args) - if not args then - return "" - end - local values = {self:short_representation(#args)} - for _, value in ipairs(args) do - values[#values + 1] = self:value_representation(value) - end - return table.concat(values) -end - --- [...][][][] -function _M:query_representation(args, options) - local consistency_repr = self:short_representation(options.consistency_level) - local args_representation = self:values_representation(args) - - -- - local flags_repr = 0 - if args then - flags_repr = utils.setbit(flags_repr, constants.query_flags.VALUES) - end - - local paging_state = "" - if options.paging_state then - flags_repr = utils.setbit(flags_repr, constants.query_flags.PAGING_STATE) - paging_state = self:bytes_representation(options.paging_state) - end - - local page_size = "" - if options.page_size > 0 then - flags_repr = utils.setbit(flags_repr, constants.query_flags.PAGE_SIZE) - page_size = self:int_representation(options.page_size) - end - - local serial_consistency = "" - if options.serial_consistency ~= nil then - flags_repr = utils.setbit(flags_repr, constants.query_flags.SERIAL_CONSISTENCY) - serial_consistency = self:short_representation(options.serial_consistency) - end - - return consistency_repr..string.char(flags_repr)..args_representation..page_size..paging_state..serial_consistency -end - --- ... -function _M:batch_representation(batch, options) - local b = {} - -- - b[#b + 1] = string.char(batch.type) - -- (number of queries) - b[#b + 1] = self:short_representation(#batch.queries) - -- (operations) - for _, query in ipairs(batch.queries) do - local kind - local string_or_id - if type(query.query) == "string" then - kind = self:boolean_representation(false) - string_or_id = self:long_string_representation(query.query) - else - kind = self:boolean_representation(true) - string_or_id = self:short_bytes_representation(query.query.id) - end - - -- ... (n can be 0, but is required) - if query.args then - b[#b + 1] = kind..string_or_id..self:values_representation(query.args) - else - b[#b + 1] = kind..string_or_id..self:short_representation(0) - end - end - - -- - b[#b + 1] = self:short_representation(options.consistency_level) - - -- ... - return table.concat(b) -end - -return _M diff --git a/src/cassandra/marshallers/marshall_v3.lua b/src/cassandra/marshallers/marshall_v3.lua deleted file mode 100644 index c658bfd..0000000 --- a/src/cassandra/marshallers/marshall_v3.lua +++ /dev/null @@ -1,123 +0,0 @@ -local utils = require "cassandra.utils" -local constants = require "cassandra.constants.constants_v3" -local Marshall_v2 = require "cassandra.marshallers.marshall_v2" - -local _M = Marshall_v2:extend() - -_M.TYPES.udt = 0x30 -_M.TYPES.tuple = 0x31 - -function _M:list_representation(elements) - local buffer = {self:int_representation(#elements)} - for _, value in ipairs(elements) do - buffer[#buffer + 1] = self:value_representation(value) - end - return table.concat(buffer) -end - -function _M:set_representation(elements) - return self:list_representation(elements) -end - -function _M:map_representation(map) - local buffer = {} - local size = 0 - for key, value in pairs(map) do - buffer[#buffer + 1] = self:value_representation(key) - buffer[#buffer + 1] = self:value_representation(value) - size = size + 1 - end - return self:int_representation(size)..table.concat(buffer) -end - -function _M:udt_representation(ordered_fields) - local buffer = {} - for _, value in ipairs(ordered_fields) do - buffer[#buffer + 1] = self:value_representation(value) - end - return table.concat(buffer) -end - -function _M:tuple_representation(ordered_fields) - return self:udt_representation(ordered_fields) -end - --- Re-definition -_M.encoders = { - -- custom=0x00, - [_M.TYPES.ascii] = _M.identity_representation, - [_M.TYPES.bigint] = _M.bigint_representation, - [_M.TYPES.blob] = _M.identity_representation, - [_M.TYPES.boolean] = _M.boolean_representation, - [_M.TYPES.counter] = _M.bigint_representation, - -- decimal=0x06, - [_M.TYPES.double] = _M.double_representation, - [_M.TYPES.float] = _M.float_representation, - [_M.TYPES.int] = _M.int_representation, - [_M.TYPES.text] = _M.identity_representation, - [_M.TYPES.timestamp] = _M.bigint_representation, - [_M.TYPES.uuid] = _M.uuid_representation, - [_M.TYPES.varchar] = _M.identity_representation, - [_M.TYPES.varint] = _M.int_representation, - [_M.TYPES.timeuuid] = _M.uuid_representation, - [_M.TYPES.inet] = _M.inet_representation, - [_M.TYPES.list] = _M.list_representation, - [_M.TYPES.map] = _M.map_representation, - [_M.TYPES.set] = _M.set_representation, - [_M.TYPES.udt] = _M.udt_representation, - [_M.TYPES.tuple] = _M.tuple_representation -} - -function _M:value_representation(value, cass_type) - local infered_type - local value_lua_type = type(value) - if cass_type then - infered_type = cass_type - elseif value_lua_type == "number" and math.floor(value) == value then - infered_type = _M.TYPES.int - elseif value_lua_type == "number" then - infered_type = _M.TYPES.float - elseif value_lua_type == "boolean" then - infered_type = _M.TYPES.boolean - elseif value_lua_type == "table" and value.type == "null" then - infered_type = self:int_representation(-1) - elseif value_lua_type == "table" and value.type and value.value then - -- Value passed as a binded parameter. - infered_type = _M.TYPES[value.type] - value = value.value - else - infered_type = _M.TYPES.varchar - end - - local representation = _M.encoders[infered_type](self, value) - return self:bytes_representation(representation) -end - --- [[name_1]...[name_n]][][][][] -function _M:query_representation(args, options) - local repr = self.super.query_representation(self, args, options) - - -- TODO timestamp - -- TODO named values - - return repr -end - --- ...[serial_consistency>][] -function _M:batch_representation(batch, options) - local repr = self.super.batch_representation(self, batch, options) - local flags_repr = 0 - - local serial_consistency = "" - if options.serial_consistency ~= nil then - flags_repr = utils.setbit(flags_repr, constants.query_flags.SERIAL_CONSISTENCY) - serial_consistency = self:short_representation(options.serial_consistency) - end - - -- TODO timestamp - -- TODO named values - - return repr..string.char(flags_repr)..serial_consistency -end - -return _M diff --git a/src/cassandra/marshallers/unmarshall_v2.lua b/src/cassandra/marshallers/unmarshall_v2.lua deleted file mode 100644 index c6f3223..0000000 --- a/src/cassandra/marshallers/unmarshall_v2.lua +++ /dev/null @@ -1,231 +0,0 @@ -local Object = require "cassandra.classic" -local Marshall_v2 = require "cassandra.marshallers.marshall_v2" - -local function read_raw_bytes(buffer, n_bytes) - local bytes = string.sub(buffer.str, buffer.pos, buffer.pos + n_bytes - 1) - buffer.pos = buffer.pos + n_bytes - return bytes -end - -local function string_to_number(str, signed) - local number = 0 - local exponent = 1 - for i = #str, 1, -1 do - number = number + string.byte(str, i) * exponent - exponent = exponent * 256 - end - if signed and number > exponent / 2 then - -- 2's complement - number = number - exponent - end - return number -end - -local _M = Object:extend() - -function _M:create_buffer(str) - return {str = str, pos = 1} -end - -function _M:read_raw_byte(buffer) - return string.byte(read_raw_bytes(buffer, 1)) -end - -function _M:read_raw(value) - return value -end - -function _M:read_short(buffer) - return string_to_number(read_raw_bytes(buffer, 2), false) -end - -function _M:read_int(buffer) - return string_to_number(read_raw_bytes(buffer, 4), true) -end - -function _M:read_signed_number(bytes) - return string_to_number(bytes, true) -end - -function _M:read_string(buffer) - local str_size = self:read_short(buffer) - return read_raw_bytes(buffer, str_size) -end - -function _M:read_boolean(bytes) - return string.byte(bytes) == 1 -end - -function _M:read_bytes(buffer) - local size = self:read_int(buffer, true) - if size < 0 then - return nil - end - return read_raw_bytes(buffer, size) -end - -function _M:read_short_bytes(buffer) - local size = self:read_short(buffer) - return read_raw_bytes(buffer, size) -end - -function _M:read_bigint(bytes) - local b1, b2, b3, b4, b5, b6, b7, b8 = string.byte(bytes, 1, 8) - if b1 < 0x80 then - return ((((((b1 * 0x100 + b2) * 0x100 + b3) * 0x100 + b4) * 0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8 - else - return ((((((((b1 - 0xFF) * 0x100 + (b2 - 0xFF)) * 0x100 + (b3 - 0xFF)) * 0x100 + (b4 - 0xFF)) * 0x100 + (b5 - 0xFF)) * 0x100 + (b6 - 0xFF)) * 0x100 + (b7 - 0xFF)) * 0x100 + (b8 - 0xFF)) - 1 - end -end - -function _M:read_double(bytes) - local b1, b2, b3, b4, b5, b6, b7, b8 = string.byte(bytes, 1, 8) - local sign = b1 > 0x7F - local exponent = (b1 % 0x80) * 0x10 + math.floor(b2 / 0x10) - local mantissa = ((((((b2 % 0x10) * 0x100 + b3) * 0x100 + b4) * 0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8 - if sign then - sign = -1 - else - sign = 1 - end - local number - if mantissa == 0 and exponent == 0 then - number = sign * 0.0 - elseif exponent == 0x7FF then - if mantissa == 0 then - number = sign * math.huge - else - number = 0.0/0.0 - end - else - number = sign * math.ldexp(1.0 + mantissa / 0x10000000000000, exponent - 0x3FF) - end - return number -end - -function _M:read_float(bytes) - local b1, b2, b3, b4 = string.byte(bytes, 1, 4) - local exponent = (b1 % 0x80) * 0x02 + math.floor(b2 / 0x80) - local mantissa = math.ldexp(((b2 % 0x80) * 0x100 + b3) * 0x100 + b4, -23) - if exponent == 0xFF then - if mantissa > 0 then - return 0 / 0 - else - mantissa = math.huge - exponent = 0x7F - end - elseif exponent > 0 then - mantissa = mantissa + 1 - else - exponent = exponent + 1 - end - if b1 >= 0x80 then - mantissa = -mantissa - end - return math.ldexp(mantissa, exponent - 0x7F) -end - -function _M:read_inet(bytes) - local buffer = {} - if #bytes == 16 then - -- ipv6 - for i = 1, #bytes, 2 do - buffer[#buffer + 1] = string.format("%02x", string.byte(bytes, i)) .. string.format("%02x", string.byte(bytes, i + 1)) - end - return table.concat(buffer, ":") - end - for i = 1, #bytes do - buffer[#buffer + 1] = string.format("%d", string.byte(bytes, i)) - end - return table.concat(buffer, ".") -end - -function _M:read_list(bytes, type) - local element_type = type.value - local buffer = self:create_buffer(bytes) - local n = self:read_short(buffer) - local elements = {} - for _ = 1, n do - elements[#elements + 1] = self:read_value(buffer, element_type, true) - end - return elements -end - -function _M:read_map(bytes, type) - local key_type = type.value[1] - local value_type = type.value[2] - local buffer = self:create_buffer(bytes) - local n = self:read_short(buffer) - local map = {} - for _ = 1, n do - local key = self:read_value(buffer, key_type, true) - map[key] = self:read_value(buffer, value_type, true) - end - return map -end - -function _M:read_option(buffer) - local type_id = self:read_short(buffer) - local type_value = nil - if type_id == Marshall_v2.TYPES.custom then - type_value = self:read_string(buffer) - elseif type_id == Marshall_v2.TYPES.list then - type_value = self:read_option(buffer) - elseif type_id == Marshall_v2.TYPES.map then - type_value = {self:read_option(buffer), self:read_option(buffer)} - elseif type_id == Marshall_v2.TYPES.set then - type_value = self:read_option(buffer) - end - return {id=type_id, value=type_value} -end - -function _M:read_uuid(bytes) - local buffer = {} - for i = 1, #bytes do - buffer[i] = string.format("%02x", string.byte(bytes, i)) - end - table.insert(buffer, 5, "-") - table.insert(buffer, 8, "-") - table.insert(buffer, 11, "-") - table.insert(buffer, 14, "-") - return table.concat(buffer) -end - -_M.decoders = { - -- custom=0x00, - [Marshall_v2.TYPES.ascii]=_M.read_raw, - [Marshall_v2.TYPES.bigint]=_M.read_bigint, - [Marshall_v2.TYPES.blob]=_M.read_raw, - [Marshall_v2.TYPES.boolean]=_M.read_boolean, - [Marshall_v2.TYPES.counter]=_M.read_bigint, - -- decimal=0x06, - [Marshall_v2.TYPES.double]=_M.read_double, - [Marshall_v2.TYPES.float]=_M.read_float, - [Marshall_v2.TYPES.int]=_M.read_signed_number, - [Marshall_v2.TYPES.text]=_M.read_raw, - [Marshall_v2.TYPES.timestamp]=_M.read_bigint, - [Marshall_v2.TYPES.uuid]=_M.read_uuid, - [Marshall_v2.TYPES.varchar]=_M.read_raw, - [Marshall_v2.TYPES.varint]=_M.read_signed_number, - [Marshall_v2.TYPES.timeuuid]=_M.read_uuid, - [Marshall_v2.TYPES.inet]=_M.read_inet, - [Marshall_v2.TYPES.list]=_M.read_list, - [Marshall_v2.TYPES.map]=_M.read_map, - [Marshall_v2.TYPES.set]=_M.read_list -} - -function _M:read_value(buffer, type, short) - local bytes - if short then - bytes = self:read_short_bytes(buffer) - else - bytes = self:read_bytes(buffer) - end - if bytes == nil then - return nil - end - - return _M.decoders[type.id](self, bytes, type) -end - -return _M diff --git a/src/cassandra/marshallers/unmarshall_v3.lua b/src/cassandra/marshallers/unmarshall_v3.lua deleted file mode 100644 index dea9ef8..0000000 --- a/src/cassandra/marshallers/unmarshall_v3.lua +++ /dev/null @@ -1,124 +0,0 @@ -local Marshall_v3 = require "cassandra.marshallers.marshall_v3" -local Unmarshall_v2 = require "cassandra.marshallers.unmarshall_v2" - -local _M = Unmarshall_v2:extend() - -function _M:read_list(bytes, type) - local element_type = type.value - local buffer = self:create_buffer(bytes) - local n = self:read_int(buffer) - local elements = {} - for _ = 1, n do - elements[#elements + 1] = self:read_value(buffer, element_type) - end - return elements -end - -function _M:read_map(bytes, type) - local key_type = type.value[1] - local value_type = type.value[2] - local buffer = self:create_buffer(bytes) - local n = self:read_int(buffer) - local map = {} - for _ = 1, n do - local key = self:read_value(buffer, key_type) - local value = self:read_value(buffer, value_type) - map[key] = value - end - return map -end - -function _M:read_udt(bytes, type) - local udt = {} - local buffer = self:create_buffer(bytes) - for _, field in ipairs(type.fields) do - local value = self:read_value(buffer, field.type) - udt[field.name] = value - end - return udt -end - -function _M:read_tuple(bytes, type) - local tuple = {} - local buffer = self:create_buffer(bytes) - for _, field in ipairs(type.fields) do - tuple[#tuple + 1] = self:read_value(buffer, field.type) - end - return tuple -end - -_M.decoders = { - -- custom=0x00, - [Marshall_v3.TYPES.ascii]=_M.read_raw, - [Marshall_v3.TYPES.bigint]=_M.read_bigint, - [Marshall_v3.TYPES.blob]=_M.read_raw, - [Marshall_v3.TYPES.boolean]=_M.read_boolean, - [Marshall_v3.TYPES.counter]=_M.read_bigint, - -- decimal=0x06, - [Marshall_v3.TYPES.double]=_M.read_double, - [Marshall_v3.TYPES.float]=_M.read_float, - [Marshall_v3.TYPES.int]=_M.read_signed_number, - [Marshall_v3.TYPES.text]=_M.read_raw, - [Marshall_v3.TYPES.timestamp]=_M.read_bigint, - [Marshall_v3.TYPES.uuid]=_M.read_uuid, - [Marshall_v3.TYPES.varchar]=_M.read_raw, - [Marshall_v3.TYPES.varint]=_M.read_signed_number, - [Marshall_v3.TYPES.timeuuid]=_M.read_uuid, - [Marshall_v3.TYPES.inet]=_M.read_inet, - [Marshall_v3.TYPES.list]=_M.read_list, - [Marshall_v3.TYPES.map]=_M.read_map, - [Marshall_v3.TYPES.set]=_M.read_list, - [Marshall_v3.TYPES.udt]=_M.read_udt, - [Marshall_v3.TYPES.tuple]=_M.read_tuple -} - -function _M:read_value(buffer, type) - local bytes = self:read_bytes(buffer) - if bytes == nil then - return nil - end - - return _M.decoders[type.id](self, bytes, type) -end - -local function read_udt_type(self, buffer, type, column_name) - local udt_ksname = self:read_string(buffer) - local udt_name = self:read_string(buffer) - local n = self:read_short(buffer) - local fields = {} - for _ = 1, n do - fields[#fields + 1] = { - name = self:read_string(buffer), - type = self:read_option(buffer) - } - end - return { - id = type.id, - udt_name = udt_name, - udt_keyspace = udt_ksname, - name = column_name, - fields = fields - } -end - -local function read_tuple_type(self, buffer, type, column_name) - local n = self:read_short(buffer) - local fields = {} - for _ = 1, n do - fields[#fields + 1] = { - type = self:read_option(buffer) - } - end - return { - id = type.id, - name = column_name, - fields = fields - } -end - -_M.type_decoders = { - [Marshall_v3.TYPES.udt] = read_udt_type, - [Marshall_v3.TYPES.tuple] = read_tuple_type -} - -return _M diff --git a/src/cassandra/protocol/reader_v2.lua b/src/cassandra/protocol/reader_v2.lua deleted file mode 100644 index df63b7c..0000000 --- a/src/cassandra/protocol/reader_v2.lua +++ /dev/null @@ -1,208 +0,0 @@ -local utils = require "cassandra.utils" -local Object = require "cassandra.classic" -local cerror = require "cassandra.error" - -local _M = Object:extend() - -function _M:new(unmarshaller, constants) - self.unmarshaller = unmarshaller - self.constants = constants -end - -function _M:read_error(buffer) - local code = self.unmarshaller:read_int(buffer) - local code_translation = self.constants.error_codes_translation[code] - local message = self.unmarshaller:read_string(buffer) - local formatted_message = string.format("Cassandra returned error (%s): %s", code_translation, message) - return cerror(formatted_message, message, code) -end - --- Make a session listen for a response and decode the received frame --- @param `session` The session on which to listen for a response. --- @return `parsed_frame` The parsed frame ready to be read. --- @return `err` Any error encountered during the receiving. -function _M:receive_frame(session) - local unmarshaller = self.unmarshaller - - local header, err = session.socket:receive(8) - if not header then - return nil, string.format("Failed to read frame header from %s:%s : %s", session.host, session.port, err) - end - local header_buffer = unmarshaller:create_buffer(header) - local version = unmarshaller:read_raw_byte(header_buffer) - if version ~= self.constants.version_codes.RESPONSE then - return nil, string.format("Invalid response version received from %s:%s", session.host, session.port) - end - local flags = unmarshaller:read_raw_byte(header_buffer) - local stream = unmarshaller:read_raw_byte(header_buffer) - local op_code = unmarshaller:read_raw_byte(header_buffer) - local length = unmarshaller:read_int(header_buffer) - - local body - if length > 0 then - body, err = session.socket:receive(length) - if not body then - return nil, string.format("Failed to read frame body from %s:%s : %s", session.host, session.port, err) - end - else - body = "" - end - - local body_buffer = unmarshaller:create_buffer(body) - return { - flags = flags, - stream = stream, - op_code = op_code, - buffer = body_buffer - } -end - -local constants = require "cassandra.constants.constants_v2" -_M.result_kind_parsers = { - [constants.result_kinds.VOID] = function() - return { - type = "VOID" - } - end, - [constants.result_kinds.ROWS] = function(self, buffer) - local metadata = self:parse_metadata(buffer) - local result = self:parse_rows(buffer, metadata) - result.type = "ROWS" - result.meta = { - has_more_pages = metadata.has_more_pages, - paging_state = metadata.paging_state - } - return result - end, - [constants.result_kinds.PREPARED] = function(self, buffer) - local id = self.unmarshaller:read_short_bytes(buffer) - local metadata = self:parse_metadata(buffer) - local result_metadata = self:parse_metadata(buffer) - assert(buffer.pos == #(buffer.str) + 1) - return { - id = id, - type = "PREPARED", - metadata = metadata, - result_metadata = result_metadata - } - end, - [constants.result_kinds.SET_KEYSPACE] = function(self, buffer) - return { - type = "SET_KEYSPACE", - keyspace = self.unmarshaller:read_string(buffer) - } - end, - [constants.result_kinds.SCHEMA_CHANGE] = function(self, buffer) - return { - type = "SCHEMA_CHANGE", - change = self.unmarshaller:read_string(buffer), - keyspace = self.unmarshaller:read_string(buffer), - table = self.unmarshaller:read_string(buffer) - } - end -} - -function _M:parse_response(response) - local result, tracing_id - - -- Check if frame is an error - if response.op_code == self.constants.op_codes.ERROR then - return nil, self:read_error(response.buffer) - end - - if response.flags == self.constants.flags.TRACING then - tracing_id = self.unmarshaller:read_uuid(string.sub(response.buffer.str, 1, 16)) - response.buffer.pos = 17 - end - - local result_kind = self.unmarshaller:read_int(response.buffer) - if _M.result_kind_parsers[result_kind] then - result = _M.result_kind_parsers[result_kind](self, response.buffer) - else - return nil, string.format("Invalid result kind: %x", result_kind) - end - - result.tracing_id = tracing_id - return result -end - -function _M:parse_column_type(buffer, column_name) - return self.unmarshaller:read_option(buffer) -end - -function _M:parse_metadata(buffer) - -- Flags parsing - local flags = self.unmarshaller:read_int(buffer) - local global_tables_spec = utils.hasbit(flags, self.constants.rows_flags.GLOBAL_TABLES_SPEC) - local has_more_pages = utils.hasbit(flags, self.constants.rows_flags.HAS_MORE_PAGES) - local columns_count = self.unmarshaller:read_int(buffer) - - -- Potential paging metadata - local paging_state - if has_more_pages then - paging_state = self.unmarshaller:read_bytes(buffer) - end - - -- Potential global_tables_spec metadata - local global_keyspace_name, global_table_name - if global_tables_spec then - global_keyspace_name = self.unmarshaller:read_string(buffer) - global_table_name = self.unmarshaller:read_string(buffer) - end - - -- Columns metadata - local columns = {} - for _ = 1, columns_count do - local ksname = global_keyspace_name - local tablename = global_table_name - if not global_tables_spec then - ksname = self.unmarshaller:read_string(buffer) - tablename = self.unmarshaller:read_string(buffer) - end - local column_name = self.unmarshaller:read_string(buffer) - local column_type = self:parse_column_type(buffer, column_name) - columns[#columns + 1] = { - name = column_name, - type = column_type, - table = tablename, - keyspace = ksname - } - end - - return { - columns = columns, - paging_state = paging_state, - columns_count = columns_count, - has_more_pages = has_more_pages - } -end - -function _M:parse_rows(buffer, metadata) - local columns = metadata.columns - local columns_count = metadata.columns_count - local rows_count = self.unmarshaller:read_int(buffer) - local values = {} - local row_mt = { - __index = function(t, i) - -- allows field access by position/index, not column name only - local column = columns[i] - if column then - return t[column.name] - end - return nil - end, - __len = function() return columns_count end - } - for _ = 1, rows_count do - local row = setmetatable({}, row_mt) - for i = 1, columns_count do - local value = self.unmarshaller:read_value(buffer, columns[i].type) - row[columns[i].name] = value - end - values[#values + 1] = row - end - assert(buffer.pos == #(buffer.str) + 1) - return values -end - -return _M diff --git a/src/cassandra/protocol/reader_v3.lua b/src/cassandra/protocol/reader_v3.lua deleted file mode 100644 index b21f114..0000000 --- a/src/cassandra/protocol/reader_v3.lua +++ /dev/null @@ -1,77 +0,0 @@ -local Reader_v2 = require "cassandra.protocol.reader_v2" - -local _M = Reader_v2:extend() - -function _M:receive_frame(session) - local unmarshaller = self.unmarshaller - - local header, err = session.socket:receive(8) - if not header then - return nil, string.format("Failed to read frame header from %s: %s", session.host, err) - end - local header_buffer = unmarshaller:create_buffer(header) - local version = unmarshaller:read_raw_byte(header_buffer) - if version ~= self.constants.version_codes.RESPONSE then - --return nil, string.format("Invalid response version received from %s", session.host) - end - local flags = unmarshaller:read_raw_byte(header_buffer) - local stream = unmarshaller:read_raw_byte(header_buffer) - local op_code = unmarshaller:read_raw_byte(header_buffer) - local length = unmarshaller:read_int(header_buffer) - - local body - if length > 0 then - body, err = session.socket:receive(length) - if not body then - return nil, string.format("Failed to read frame body from %s: %s", session.host, err) - end - else - body = "" - end - - local body_buffer = unmarshaller:create_buffer(body) - return { - flags = flags, - stream = stream, - op_code = op_code, - buffer = body_buffer - } -end - -function _M:parse_column_type(buffer, column_name) - local column_type = self.unmarshaller:read_option(buffer) - -- Decode UDTs and Tuples - if self.unmarshaller.type_decoders[column_type.id] then - column_type = self.unmarshaller.type_decoders[column_type.id](self.unmarshaller, buffer, column_type, column_name) - end - return column_type -end - -local constants = require "cassandra.constants.constants_v3" -_M.result_kind_parsers = { - [constants.result_kinds.SCHEMA_CHANGE] = function(self, buffer) - local change_type = self.unmarshaller:read_string(buffer) - local target = self.unmarshaller:read_string(buffer) - local ksname = self.unmarshaller:read_string(buffer) - local tablename, user_type_name - if target == "TABLE" then - tablename = self.unmarshaller:read_string(buffer) - elseif target == "TYPE" then - user_type_name = self.unmarshaller:read_string(buffer) - end - - return { - type = "SCHEMA_CHANGE", - change = self.unmarshaller:read_string(buffer), - keyspace = self.unmarshaller:read_string(buffer), - table = self.unmarshaller:read_string(buffer), - change_type = change_type, - target = target, - keyspace = ksname, - table = tablename, - user_type = user_type_name - } - end -} - -return _M diff --git a/src/cassandra/protocol/writer_v2.lua b/src/cassandra/protocol/writer_v2.lua deleted file mode 100644 index deee567..0000000 --- a/src/cassandra/protocol/writer_v2.lua +++ /dev/null @@ -1,43 +0,0 @@ -local Object = require "cassandra.classic" - -local _M = Object:extend() - -function _M:new(marshaller, constants) - self.marshaller = marshaller - self.constants = constants -end - -function _M:build_frame(op_code, body, tracing) - local version = string.char(self.constants.version_codes.REQUEST) - local flags = tracing and self.constants.flags.TRACING or "\000" - local stream_id = "\000" - local length = self.marshaller:int_representation(#body) - local frame = version..flags..stream_id..string.char(op_code)..length..body - return frame -end - --- Query: --- Batch: ...[][] -function _M:build_body(operation, args, options) - local op_code, op_repr, op_parameters = "", "", "" - if type(operation) == "string" then - -- Raw string query - op_code = self.constants.op_codes.QUERY - op_repr = self.marshaller:long_string_representation(operation) - op_parameters = self.marshaller:query_representation(args, options) - elseif operation.id then - -- Prepared statement - op_code = self.constants.op_codes.EXECUTE - op_repr = self.marshaller:short_bytes_representation(operation.id) - op_parameters = self.marshaller:query_representation(args, options) - elseif operation.is_batch_statement then - -- Batch statement - op_code = self.constants.op_codes.BATCH - op_repr = self.marshaller:batch_representation(operation, options) - end - - -- frame body - return op_repr..op_parameters, op_code -end - -return _M diff --git a/src/cassandra/protocol/writer_v3.lua b/src/cassandra/protocol/writer_v3.lua deleted file mode 100644 index 2141b12..0000000 --- a/src/cassandra/protocol/writer_v3.lua +++ /dev/null @@ -1,14 +0,0 @@ -local Writer_v2 = require "cassandra.protocol.writer_v2" - -local _M = Writer_v2:extend() - -function _M:build_frame(op_code, body, tracing) - local version = string.char(self.constants.version_codes.REQUEST) - local flags = tracing and self.constants.flags.TRACING or "\000" - local stream_id = self.marshaller:short_representation(0) - local length = self.marshaller:int_representation(#body) - local frame = version..flags..stream_id..string.char(op_code)..length..body - return frame -end - -return _M diff --git a/src/cassandra/session.lua b/src/cassandra/session.lua deleted file mode 100644 index dd31936..0000000 --- a/src/cassandra/session.lua +++ /dev/null @@ -1,303 +0,0 @@ --------- --- This module provides a session to interact with a Cassandra cluster. --- A session must be opened, can be reused and closed once you're done with it. --- In the context of Nginx, a session used the underlying cosocket API which allows --- one to put a socket in the connection pool, before reusing it later. Otherwise, --- we fallback on luasocket as the underlying socket implementation. --- @module Session - -local utils = require "cassandra.utils" -local cerror = require "cassandra.error" - -local _M = { - CQL_VERSION = "3.0.0" -} - -_M.__index = _M - -function _M:send_frame_and_get_response(op_code, frame_body, tracing) - local bytes, response, err - local frame = self.writer:build_frame(op_code, frame_body, tracing) - bytes, err = self.socket:send(frame) - if not bytes then - return nil, cerror(string.format("Failed to send frame to %s: %s:%s", self.host, self.port, err)) - end - response, err = self.reader:receive_frame(self) - if not response then - return nil, cerror(err) - end - return response -end - --- Answer an AUTHENTICATE reply from the server --- The STARTUP message is susceptible to receive an authenticate --- challenge from the server. In that case we use one of the provided --- authenticator depending on the authenticator set in Cassandra. --- @private --- @param self The session, since this method is not public. --- @param resposne The response received by the startup message. --- @return ok A boolean indicating wether or not the authentication was successful. --- @return err Any server/client `Error` encountered during the authentication. -local function answer_auth(self, response) - if not self.authenticator then - return false, cerror("Remote end requires authentication") - end - - return self.authenticator:authenticate(self) -end - -local function startup(self) - local frame_body = self.marshaller:string_map_representation({CQL_VERSION = _M.CQL_VERSION}) - local response, err = self:send_frame_and_get_response(self.constants.op_codes.STARTUP, frame_body) - if not response then - return false, err - end - - if response.op_code == self.constants.op_codes.AUTHENTICATE then - return answer_auth(self, response) - elseif response.op_code == self.constants.op_codes.ERROR then - return false, self.reader:read_error(response.buffer) - elseif response.op_code ~= self.constants.op_codes.READY then - return false, cerror("server is not ready") - end - return true -end - ---- Socket functions. --- @section Socket - ---- Connect a session to a node coordinator. --- @raise Any error due to a wrong usage of the driver (invalid parameter, non correctly initialized session...). --- @param contact_points A string or an array of strings containing the IP addresse(s) to connect to. --- Strings can be of the form "host:port" if some nodes are running on another --- port than the specified or default one. --- @param port Default: 9042. The port on which to connect to. --- @param options Options for the connection. --- `auth`: An authenticator if remote requires authentication. See `auth.lua`. --- `ssl`: A boolean indicating if the connection must use SSL. --- `ssl_verify`: A boolean indicating whether to perform SSL verification. If using --- nginx, see the `lua_ssl_trusted_certificate` directive. If using Luasocket, --- see the `ca_file` option. See the `ssl.lua` example --- `ca_file`: Path to the certificate authority file. See the `ssl.lua` example. --- @return connected boolean indicating the success of the connection. --- @return err Any server/client `Error` encountered during the connection. --- @usage local ok, err = session:connect("127.0.0.1", 9042) --- @usage local ok, err = session:connect({"127.0.0.1", "52.5.149.55:9888"}, 9042) -function _M:connect(contact_points, port, options) - if port == nil then port = 9042 end - if options == nil then options = {} end - - if contact_points == nil then - error("no contact points provided", 2) - elseif type(contact_points) == "table" then - -- shuffle the contact points so we don't try to always connect on the same order, - -- avoiding pressure on the same node coordinator. - contact_points = utils.shuffle_array(contact_points) - else - contact_points = {contact_points} - end - - local ok, err - for _, contact_point in ipairs(contact_points) do - -- Extract port if string is of the form "host:port" - local host, host_port = utils.split_by_colon(contact_point) - if not host_port then -- Default port is the one given as parameter - host_port = port - end - - ok, err = self.socket:connect(host, host_port) - if ok == 1 then - self.host = host - self.port = host_port - break - end - end - - if not ok then - return false, cerror(err) - end - - if options.ssl then - if self.socket_type == "luasocket" then - local res - ok, res = pcall(require, "ssl") - if not ok and string.find(res, "module '.*' not found") then - return false, cerror("LuaSec not found. Please install LuaSec to use SSL.") - end - local ssl = res - local params = { - mode = "client", - protocol = "tlsv1", - cafile = options.ca_file, - verify = options.ssl_verify and "peer" or "none", - options = "all" - } - - self.socket, err = ssl.wrap(self.socket, params) - if err then - return false, cerror(err) - end - - ok, err = self.socket:dohandshake() - if not ok then - return false, cerror(err) - end - else - ok, err = self.socket:sslhandshake(false, nil, options.ssl_verify) - if not ok then - return false, cerror(err) - end - end - end - - self.authenticator = options.authenticator - - if self.socket_type ~= "ngx" or self:get_reused_times() < 1 then - self.ready, err = startup(self) - if not self.ready then - return false, err - end - end - - return true -end - ---- Change the timeout value of the underlying socket object. --- Wrapper around the cosocket (or luasocket) "settimeout()" depending on --- what context you are using it. --- See the related implementation of "settimeout()" for parameters. --- @raise Exception if the session does not have an underlying socket (not correctly initialized). --- @see tcpsock:settimeout() --- @see luasocket:settimeout() --- @return The underlying result from tcpsock or luasocket. -function _M:set_timeout(...) - return self.socket:settimeout(...) -end - ---- Put the underlying socket into the cosocket connection pool. --- This method is only available when using the cosocket API. --- Wrapper around the cosocket "setkeepalive()" method. --- @raise Exception if the session does not have an underlying socket (not correctly initialized). --- @see tcpsock:setkeepalive() -function _M:set_keepalive(...) - if not self.socket.setkeepalive then - return nil, cerror("luasocket does not support reusable sockets") - end - return self.socket:setkeepalive(...) -end - ---- Return the number of successfully reused times for the underlying socket. --- This method is only available when using the cosocket API. --- Wrapper round the cosocket "getreusedtimes()" method. --- @raise Exception if the session does not have an underlying socket (not correctly initialized). --- @see tcpsock:getreusedtimes() -function _M:get_reused_times() - if not self.socket.getreusedtimes then - return nil, cerror("luasocket does not support reusable sockets") - end - return self.socket:getreusedtimes() -end - ---- Close a connected session. --- Wrapper around the cosocket (or luasocket) "close()" depending on --- what context you are using it. --- @raise Exception if the session does not have an underlying socket (not correctly initialized). --- @see tcpsock:close() --- @see luasocket:close() --- @return The underlying closing result from tcpsock or luasocket -function _M:close() - return self.socket:close() -end - ---- Default query options. --- @see `:execute()` -local default_options = { - page_size = 5000, - auto_paging = false, - tracing = false -} - -local function page_iterator(session, operation, args, options) - local page = 0 - local rows, err - return function(paginated_operation, previous_rows) - if previous_rows and previous_rows.meta.has_more_pages == false then - return nil -- End iteration after error - end - - rows, err = session:execute(paginated_operation, args, { - page_size = options.page_size, - paging_state = previous_rows and previous_rows.meta.paging_state - }) - - -- If we have some results, increment the page - if rows ~= nil and #rows > 0 then - page = page + 1 - else - if err then - -- Just expose the error with 1 last iteration - return {meta={has_more_pages=false}}, err, page - elseif rows.meta.has_more_pages == false then - return nil -- End of the iteration - end - end - - return rows, err, page - end, operation, nil -end - ---- Queries functions. --- @section operations - ---- Execute an operation (query, prepared statement, batch statement). --- @param operation The operation to execute. Whether it being a plain string query, a prepared statement or a batch. --- @param args (Optional) An array of arguments to bind to the operation if it is a query or a statement. --- @param options (Optional) A table of options for this query. --- @return response The parsed response from Cassandra. --- @return err Any `Error` encountered during the execution. -function _M:execute(operation, args, options) - if not options then options = {} end - -- Default options - if not options.consistency_level then - options.consistency_level = self.constants.consistency.ONE - end - for k in pairs(default_options) do - if options[k] == nil then options[k] = default_options[k] end - end - - if options.auto_paging then - return page_iterator(self, operation, args, options) - end - - local frame_body, op_code = self.writer:build_body(operation, args, options) - local response, err = self:send_frame_and_get_response(op_code, frame_body, options.tracing) - if not response then - return nil, err - end - - return self.reader:parse_response(response) -end - ---- Set a keyspace for that session. --- Execute a "USE keyspace_name" query. --- @param keyspace Name of the keyspace to use. --- @return Results from @{execute}. -function _M:set_keyspace(keyspace) - return self:execute(string.format("USE \"%s\"", keyspace)) -end - ---- Prepare a query. --- @param query The query to prepare. --- @param tracing A boolean indicating if the preparation of this query should be traced. --- @return statement A prepared statement to be given to @{execute}. -function _M:prepare(query, tracing) - local frame_body = self.marshaller:long_string_representation(query) - local response, err = self:send_frame_and_get_response(self.constants.op_codes.PREPARE, frame_body, tracing) - if not response then - return nil, err - end - - return self.reader:parse_response(response) -end - -return _M diff --git a/src/cassandra/utils.lua b/src/cassandra/utils.lua deleted file mode 100644 index b56ec79..0000000 --- a/src/cassandra/utils.lua +++ /dev/null @@ -1,112 +0,0 @@ -local CONSTS = require "cassandra.consts" - -local _M = {} - -function _M.big_endian_representation(num, bytes) - if num < 0 then - -- 2's complement - num = math.pow(0x100, bytes) + num - end - local t = {} - while num > 0 do - local rest = math.fmod(num, 0x100) - table.insert(t, 1, string.char(rest)) - num = (num-rest) / 0x100 - end - local padding = string.rep(string.char(0), bytes - #t) - return padding .. table.concat(t) -end - -function _M.string_to_number(str, signed) - local number = 0 - local exponent = 1 - for i = #str, 1, -1 do - number = number + string.byte(str, i) * exponent - exponent = exponent * 256 - end - if signed and number > exponent / 2 then - -- 2's complement - number = number - exponent - end - return number -end - -math.randomseed(os.time()) - --- @see http://en.wikipedia.org/wiki/Fisher-Yates_shuffle -function _M.shuffle_array(arr) - local n = #arr - while n >= 2 do - local k = math.random(n) - arr[n], arr[k] = arr[k], arr[n] - n = n - 1 - end - return arr -end - -function _M.extend_table(defaults, values) - for k in pairs(defaults) do - if values[k] == nil then - values[k] = defaults[k] - end - end -end - -function _M.is_array(t) - local i = 0 - for _ in pairs(t) do - i = i + 1 - if t[i] == nil and t[tostring(i)] == nil then return false end - end - return true -end - -function _M.split_by_colon(str) - local fields = {} - str:gsub("([^:]+)", function(c) fields[#fields+1] = c end) - return fields[1], fields[2] -end - -function _M.hasbit(x, p) - return x % (p + p) >= p -end - -function _M.setbit(x, p) - return _M.hasbit(x, p) and x or x + p -end - -function _M.deep_copy(orig) - local copy - if type(orig) == "table" then - copy = {} - for orig_key, orig_value in next, orig, nil do - copy[_M.deep_copy(orig_key)] = _M.deep_copy(orig_value) - end - setmetatable(copy, _M.deep_copy(getmetatable(orig))) - else - copy = orig - end - return copy -end - -local rawget = rawget - -local _const_mt = { - get = function(t, key, version) - if not version then version = CONSTS.MAX_PROTOCOL_VERSION end - - local const, version_consts - while version >= CONSTS.MIN_PROTOCOL_VERSION and const == nil do - version_consts = t[version] ~= nil and t[version] or t - const = rawget(version_consts, key) - version = version - 1 - end - return const - end -} - -_const_mt.__index = _const_mt - -_M.const_mt = _const_mt - -return _M diff --git a/src/cassandra/v2.lua b/src/cassandra/v2.lua deleted file mode 100644 index 3a2aacb..0000000 --- a/src/cassandra/v2.lua +++ /dev/null @@ -1,3 +0,0 @@ -local _cassandra = require "_cassandra" - -return _cassandra("v2") From df503a13cde1d7f4119a97e023f4817748ea6f08 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Wed, 4 Nov 2015 16:23:05 -0800 Subject: [PATCH 11/78] feat(rewrite) new cql types and more buffer specs - CQL specs for both protocol v1 and v2 - set, uuid, map types - few fixes --- spec/unit/buffer_spec.lua | 23 ++++++------ spec/unit/cql_types_buffer_spec.lua | 54 +++++++++++++++++++++++------ src/cassandra/buffer.lua | 27 +++++++++++---- src/cassandra/consts.lua | 10 ++++-- src/cassandra/types/bytes.lua | 2 +- src/cassandra/types/map.lua | 42 ++++++++++++++++++++++ src/cassandra/types/options.lua | 4 ++- src/cassandra/types/set.lua | 13 ++++--- src/cassandra/types/uuid.lua | 28 +++++++++++++++ src/cassandra/utils/buffer.lua | 13 +++---- 10 files changed, 169 insertions(+), 47 deletions(-) create mode 100644 src/cassandra/types/map.lua create mode 100644 src/cassandra/types/uuid.lua diff --git a/spec/unit/buffer_spec.lua b/spec/unit/buffer_spec.lua index f254b95..c3f19a8 100644 --- a/spec/unit/buffer_spec.lua +++ b/spec/unit/buffer_spec.lua @@ -5,9 +5,9 @@ describe("Buffer", function() byte = {1, 2, 3}, int = {0, 4200, -42}, short = {0, 1, -1, 12, 13}, - --boolean = {true, false}, string = {"hello world"}, long_string = {string.rep("blob", 1000), ""}, + uuid = {"1144bada-852c-11e3-89fb-e0b9a54a6d11"}, inet = { "127.0.0.1", "0.0.0.1", "8.8.8.8", "2001:0db8:85a3:0042:1000:8a2e:0370:7334", @@ -39,19 +39,20 @@ describe("Buffer", function() end it("should accumulate values", function() - local writer = Buffer(3) -- protocol v3 - writer:write_byte(2) - writer:write_int(128) - writer:write_string("hello world") + local buf = Buffer(3) -- protocol v3 + buf:write_byte(2) + buf:write_int(128) + buf:write_string("hello world") - local reader = Buffer.from_buffer(writer) - assert.equal(2, reader:read_byte()) - assert.equal(128, reader:read_int()) - assert.equal("hello world", reader:read_string()) + buf:reset() + + assert.equal(2, buf:read_byte()) + assert.equal(128, buf:read_int()) + assert.equal("hello world", buf:read_string()) end) describe("inet", function() - local fixtures = { + local FIXTURES = { ["2001:0db8:85a3:0042:1000:8a2e:0370:7334"] = "2001:0db8:85a3:0042:1000:8a2e:0370:7334", ["2001:0db8:0000:0000:0000:0000:0000:0001"] = "2001:db8::1", ["2001:0db8:85a3:0000:0000:0000:0000:0010"] = "2001:db8:85a3::10", @@ -61,7 +62,7 @@ describe("Buffer", function() } it("should shorten ipv6 addresses", function() - for expected_ip, fixture_ip in pairs(fixtures) do + for expected_ip, fixture_ip in pairs(FIXTURES) do local buffer = Buffer(3) buffer:write_inet(fixture_ip) buffer.pos = 1 diff --git a/spec/unit/cql_types_buffer_spec.lua b/spec/unit/cql_types_buffer_spec.lua index 870c1ff..4ebb190 100644 --- a/spec/unit/cql_types_buffer_spec.lua +++ b/spec/unit/cql_types_buffer_spec.lua @@ -1,6 +1,10 @@ local Buffer = require "cassandra.buffer" +local CONSTS = require "cassandra.consts" +local CQL_TYPES = require "cassandra.types.cql_types" -describe("CQL Types", function() +for _, protocol_version in ipairs(CONSTS.SUPPORTED_PROTOCOL_VERSION) do + +describe("CQL Types protocol v"..protocol_version, function() local FIXTURES = { boolean = {true, false}, inet = { @@ -9,22 +13,17 @@ describe("CQL Types", function() "2001:0db8:0000:0000:0000:0000:0000:0001" }, int = {0, 4200, -42}, - set = { - {"abc", "def"}, - {0, 1, 2, 42, -42} - }, + uuid = {"1144bada-852c-11e3-89fb-e0b9a54a6d11"} } for fixture_type, fixture_values in pairs(FIXTURES) do it("["..fixture_type.."] should be bufferable", function() for _, fixture in ipairs(fixture_values) do - local writer = Buffer(3) - writer["write_cql_"..fixture_type](writer, fixture) - local bytes = writer:dump() - - local reader = Buffer(3, bytes) -- protocol v3 - local decoded = reader["read_cql_"..fixture_type](reader) + local buf = Buffer(protocol_version) + buf["write_cql_"..fixture_type](buf, fixture) + buf:reset() + local decoded = buf["read_cql_"..fixture_type](buf) if type(fixture) == "table" then assert.same(fixture, decoded) else @@ -33,4 +32,37 @@ describe("CQL Types", function() end end) end + + it("[map] should be bufferable", function() + local MAP_FIXTURES = { + {key_type = CQL_TYPES.text, value_type = CQL_TYPES.text, value = {k1 = "v1", k2 = "v2"}}, + {key_type = CQL_TYPES.text, value_type = CQL_TYPES.int, value = {k1 = 1, k2 = 2}}, + {key_type = CQL_TYPES.text, value_type = CQL_TYPES.int, value = {}} + } + + for _, fixture in ipairs(MAP_FIXTURES) do + local buf = Buffer(protocol_version) + buf:write_cql_map(fixture.value) + buf:reset() + local decoded = buf:read_cql_map({{id = fixture.key_type}, {id = fixture.value_type}}) + assert.same(fixture.value, decoded) + end + end) + + it("[set] should be bufferable", function() + local SET_FIXTURES = { + {value_type = CQL_TYPES.text, value = {"abc", "def"}}, + {value_type = CQL_TYPES.int, value = {1, 2 , 0, -42, 42}} + } + + for _, fixture in ipairs(SET_FIXTURES) do + local buf = Buffer(protocol_version) + buf:write_cql_set(fixture.value) + buf:reset() + local decoded = buf:read_cql_set({id = fixture.value_type}) + assert.same(fixture.value, decoded) + end + end) end) + +end diff --git a/src/cassandra/buffer.lua b/src/cassandra/buffer.lua index 1d3a306..8d38aef 100644 --- a/src/cassandra/buffer.lua +++ b/src/cassandra/buffer.lua @@ -1,5 +1,6 @@ local Buffer = require "cassandra.utils.buffer" local CQL_TYPES = require "cassandra.types.cql_types" +local t_utils = require "cassandra.utils.table" local math_floor = math.floor --- Frame types @@ -12,7 +13,7 @@ local TYPES = { "short", "string", "long_string", - -- "uuid", + "uuid", -- "string_list", "bytes", -- "short_bytes", @@ -49,11 +50,11 @@ local CQL_TYPES_ = { "inet", "int", -- "list", - -- "map", + "map", "set", -- "text", -- "timestamp", - -- "uuid", + "uuid", -- "varchar", -- "varint", -- "timeuuid", @@ -102,7 +103,7 @@ local DECODER_NAMES = { [CQL_TYPES.tuple] = "tuple" } -function Buffer:write_cql_value(value, assumed_type) +function Buffer:repr_cql_value(value, assumed_type) local infered_type local lua_type = type(value) @@ -110,14 +111,26 @@ function Buffer:write_cql_value(value, assumed_type) infered_type = assumed_type elseif lua_type == "number" and math_floor(value) == value then infered_type = CQL_TYPES.int + elseif lua_type == "table" then + if t_utils.is_array(value) then + infered_type = CQL_TYPES.set + else + infered_type = CQL_TYPES.map + end + else + infered_type = CQL_TYPES.varchar end - local encoder = "write_cql_"..DECODER_NAMES[infered_type] - Buffer[encoder](self, value) + local encoder = "repr_cql_"..DECODER_NAMES[infered_type] + return Buffer[encoder](self, value) +end + +function Buffer:write_cql_value(...) + self:write(self:repr_cql_value(...)) end function Buffer:read_cql_value(assumed_type) - local decoder = "read_cql_"..DECODER_NAMES[assumed_type.type_id] + local decoder = "read_cql_"..DECODER_NAMES[assumed_type.id] return Buffer[decoder](self, assumed_type.value) end diff --git a/src/cassandra/consts.lua b/src/cassandra/consts.lua index a725e01..5c2bdb3 100644 --- a/src/cassandra/consts.lua +++ b/src/cassandra/consts.lua @@ -1,7 +1,11 @@ +local SUPPORTED_PROTOCOL_VERSION = {2, 3} +local DEFAULT_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSION[#SUPPORTED_PROTOCOL_VERSION] + return { - DEFAULT_PROTOCOL_VERSION = 3, - MIN_PROTOCOL_VERSION = 2, - MAX_PROTOCOL_VERSION = 3, + SUPPORTED_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSION, + DEFAULT_PROTOCOL_VERSION = DEFAULT_PROTOCOL_VERSION, + MIN_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSION[1], + MAX_PROTOCOL_VERSION = DEFAULT_PROTOCOL_VERSION, CQL_VERSION = "3.0.0", DEFAULT_CQL_PORT = 9042 } diff --git a/src/cassandra/types/bytes.lua b/src/cassandra/types/bytes.lua index 432fbf6..9f4e79c 100644 --- a/src/cassandra/types/bytes.lua +++ b/src/cassandra/types/bytes.lua @@ -5,7 +5,7 @@ return { return int.repr(nil, #val)..val end, read = function(self) - local n_bytes = int.read(self) + local n_bytes = self:read_int() return self:read(n_bytes) end } diff --git a/src/cassandra/types/map.lua b/src/cassandra/types/map.lua new file mode 100644 index 0000000..7ff43b1 --- /dev/null +++ b/src/cassandra/types/map.lua @@ -0,0 +1,42 @@ +local table_insert = table.insert +local table_concat = table.concat + +return { + repr = function(self, map) + local repr = {} + local size = 0 + + for key, value in pairs(map) do + repr[#repr + 1] = self:repr_cql_value(key) + repr[#repr + 1] = self:repr_cql_value(value) + size = size + 1 + end + + if self.version < 3 then + table_insert(repr, 1, self:repr_short(size)) + else + table_insert(repr, 1, self:repr_int(size)) + end + + return table_concat(repr) + end, + read = function(buffer, type) + local map = {} + local key_type = type[1] + local value_type = type[2] + + local n + if buffer.version < 3 then + n = buffer:read_short() + else + n = buffer:read_int() + end + + for _ = 1, n do + local key = buffer:read_cql_value(key_type) + map[key] = buffer:read_cql_value(value_type) + end + + return map + end +} diff --git a/src/cassandra/types/options.lua b/src/cassandra/types/options.lua index 125e91e..4ecc29e 100644 --- a/src/cassandra/types/options.lua +++ b/src/cassandra/types/options.lua @@ -6,9 +6,11 @@ return { local type_value if type_id == CQL_TYPES.set then type_value = buffer:read_options() + elseif type_id == CQL_TYPES.map then + type_value = {buffer:read_options(), buffer:read_options()} end -- @TODO support non-native types (custom, map, list, set, UDT, tuple) - return {type_id = type_id, value = type_value} + return {id = type_id, value = type_value} end } diff --git a/src/cassandra/types/set.lua b/src/cassandra/types/set.lua index be7d5c0..b5cf7b4 100644 --- a/src/cassandra/types/set.lua +++ b/src/cassandra/types/set.lua @@ -1,14 +1,19 @@ +local table_concat = table.concat +local table_insert = table.insert + return { repr = function(self, set) - local n + local repr = {} if self.version < 3 then - n = self:repr_short(#set) + table_insert(repr, self:repr_short(#set)) else - n = self:repr_int(#set) + table_insert(repr, self:repr_int(#set)) end for _, val in ipairs(set) do - -- @TODO write_value infering the type + table_insert(repr, self:repr_cql_value(val)) end + + return table_concat(repr) end, read = function(buffer, value_type) local n diff --git a/src/cassandra/types/uuid.lua b/src/cassandra/types/uuid.lua new file mode 100644 index 0000000..c83b7db --- /dev/null +++ b/src/cassandra/types/uuid.lua @@ -0,0 +1,28 @@ +local string_gsub = string.gsub +local string_sub = string.sub +local string_format = string.format +local table_insert = table.insert +local table_concat = table.concat + +return { + repr = function(self, val) + local repr = {} + local str = string_gsub(val, "-", "") + for i = 1, #str, 2 do + local byte_str = string_sub(str, i, i + 1) + table_insert(repr, self:repr_byte(tonumber(byte_str, 16))) + end + return table_concat(repr) + end, + read = function(buffer) + local uuid = {} + for i = 1, buffer.len do + uuid[i] = string_format("%02x", buffer:read_byte()) + end + table_insert(uuid, 5, "-") + table_insert(uuid, 8, "-") + table_insert(uuid, 11, "-") + table_insert(uuid, 14, "-") + return table_concat(uuid) + end +} diff --git a/src/cassandra/utils/buffer.lua b/src/cassandra/utils/buffer.lua index 526c359..b3235e3 100644 --- a/src/cassandra/utils/buffer.lua +++ b/src/cassandra/utils/buffer.lua @@ -8,8 +8,9 @@ local Buffer = Object:extend() function Buffer:new(version, str) self.version = version -- protocol version for properly encoding types self.str = str and str or "" - self.pos = 1 -- lua indexes start at 1, remember? + self.pos = nil self.len = #self.str + self:reset() end function Buffer:dump() @@ -30,14 +31,8 @@ function Buffer:read(n_bytes_to_read) return bytes end -function Buffer.from_buffer(buffer) - return Buffer(buffer.version, buffer:dump()) -end - -function Buffer.copy(buffer) - local b = Buffer(buffer.version, buffer:dump()) - b.pos = buffer.pos - return b +function Buffer:reset() + self.pos = 1 -- lua indexes start at 1, remember? end return Buffer From 1204b4b691216db956534260c33616cbd0e81491 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Wed, 4 Nov 2015 16:39:54 -0800 Subject: [PATCH 12/78] rewrite(buffer) cql_decoders grouped with decoder initializers --- src/cassandra/buffer.lua | 83 +++++++++++++++------------------------- 1 file changed, 30 insertions(+), 53 deletions(-) diff --git a/src/cassandra/buffer.lua b/src/cassandra/buffer.lua index 8d38aef..89dbfae 100644 --- a/src/cassandra/buffer.lua +++ b/src/cassandra/buffer.lua @@ -38,71 +38,48 @@ end --- CQL Types -- @section cql_types -local CQL_TYPES_ = { - "raw", - -- "ascii", - -- "biging", - -- "blob", - "boolean", - -- "decimal", - -- "double", - -- "float", - "inet", - "int", - -- "list", - "map", - "set", - -- "text", - -- "timestamp", - "uuid", - -- "varchar", - -- "varint", - -- "timeuuid", - -- "tuple" +local CQL_DECODERS = { + -- custom = 0x00, + [CQL_TYPES.ascii] = "raw", + -- [CQL_TYPES.bigint] = "bigint", + [CQL_TYPES.blob] = "raw", + [CQL_TYPES.boolean] = "boolean", + -- [CQL_TYPES.counter] = "counter", + -- decimal 0x06 + -- [CQL_TYPES.double] = "double", + -- [CQL_TYPES.float] = "float", + [CQL_TYPES.inet] = "inet", + [CQL_TYPES.int] = "int", + [CQL_TYPES.text] = "raw", + [CQL_TYPES.list] = "set", + [CQL_TYPES.map] = "map", + [CQL_TYPES.set] = "set", + [CQL_TYPES.uuid] = "uuid", + -- [CQL_TYPES.timestamp] = "timestamp", + [CQL_TYPES.varchar] = "raw", + -- [CQL_TYPES.varint] = "varint", + -- [CQL_TYPES.timeuuid] = "timeuuid", + -- [CQL_TYPES.udt] = "udt", + -- [CQL_TYPES.tuple] = "tuple" } -for _, cql_type in ipairs(CQL_TYPES_) do - local mod = require("cassandra.types."..cql_type) - Buffer["repr_cql_"..cql_type] = function(self, ...) +for _, cql_decoder in pairs(CQL_DECODERS) do + local mod = require("cassandra.types."..cql_decoder) + Buffer["repr_cql_"..cql_decoder] = function(self, ...) local repr = mod.repr(self, ...) return self:repr_bytes(repr) end - Buffer["write_cql_"..cql_type] = function(self, ...) + Buffer["write_cql_"..cql_decoder] = function(self, ...) local repr = mod.repr(self, ...) self:write_bytes(repr) end - Buffer["read_cql_"..cql_type] = function(self, ...) + Buffer["read_cql_"..cql_decoder] = function(self, ...) local bytes = self:read_bytes() local buf = Buffer(self.version, bytes) return mod.read(buf, ...) end end -local DECODER_NAMES = { - -- custom = 0x00, - [CQL_TYPES.ascii] = "raw", - [CQL_TYPES.bigint] = "bigint", - [CQL_TYPES.blob] = "raw", - [CQL_TYPES.boolean] = "boolean", - [CQL_TYPES.counter] = "counter", - -- decimal 0x06 - [CQL_TYPES.double] = "double", - [CQL_TYPES.float] = "float", - [CQL_TYPES.int] = "int", - [CQL_TYPES.text] = "raw", - [CQL_TYPES.timestamp] = "timestamp", - [CQL_TYPES.uuid] = "uuid", - [CQL_TYPES.varchar] = "raw", - [CQL_TYPES.varint] = "varint", - [CQL_TYPES.timeuuid] = "timeuuid", - [CQL_TYPES.inet] = "inet", - [CQL_TYPES.list] = "list", - [CQL_TYPES.map] = "map", - [CQL_TYPES.set] = "set", - [CQL_TYPES.udt] = "udt", - [CQL_TYPES.tuple] = "tuple" -} - function Buffer:repr_cql_value(value, assumed_type) local infered_type local lua_type = type(value) @@ -121,7 +98,7 @@ function Buffer:repr_cql_value(value, assumed_type) infered_type = CQL_TYPES.varchar end - local encoder = "repr_cql_"..DECODER_NAMES[infered_type] + local encoder = "repr_cql_"..CQL_DECODERS[infered_type] return Buffer[encoder](self, value) end @@ -130,7 +107,7 @@ function Buffer:write_cql_value(...) end function Buffer:read_cql_value(assumed_type) - local decoder = "read_cql_"..DECODER_NAMES[assumed_type.id] + local decoder = "read_cql_"..CQL_DECODERS[assumed_type.id] return Buffer[decoder](self, assumed_type.value) end From b2bb3ea0fac8bba9c978929aacf418c65e0349bb Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Wed, 4 Nov 2015 19:46:14 -0800 Subject: [PATCH 13/78] feat(rewrite) cluster info retrieval on init and logger object --- spec/integration/client_spec.lua | 22 +++-- src/cassandra/client.lua | 10 +- src/cassandra/client_options.lua | 14 ++- src/cassandra/consts.lua | 3 +- src/cassandra/control_connection.lua | 92 ++++++++++++++----- src/cassandra/frame_reader.lua | 7 +- src/cassandra/host.lua | 14 ++- src/cassandra/host_connection.lua | 24 +++-- src/cassandra/log.lua | 29 ------ src/cassandra/logger.lua | 39 ++++++++ src/cassandra/policies/address_resolution.lua | 9 ++ src/cassandra/request_handler.lua | 2 +- src/cassandra/types/frame_header.lua | 2 - src/cassandra/utils/table.lua | 3 + 14 files changed, 181 insertions(+), 89 deletions(-) delete mode 100644 src/cassandra/log.lua create mode 100644 src/cassandra/logger.lua create mode 100644 src/cassandra/policies/address_resolution.lua diff --git a/spec/integration/client_spec.lua b/spec/integration/client_spec.lua index ea4db02..a6dd4da 100644 --- a/spec/integration/client_spec.lua +++ b/spec/integration/client_spec.lua @@ -1,39 +1,43 @@ local Client = require "cassandra.client" +local t_utils = require "cassandra.utils.table" local FAKE_CLUSTER = {"0.0.0.1", "0.0.0.2", "0.0.0.3"} -local CLUSTER_2_0 = {"127.0.0.1:9001", "127.0.0.1:9002", "127.0.0.1:9003"} -local CLUSTER_2_1 = {"127.0.0.1:9101", "127.0.0.1:9102", "127.0.0.1:9103"} +local contact_points_2_0 = {"127.0.0.1:9001"} +local contact_points_2_1 = {"127.0.0.1:9101"} + +local function client_factory(opts) + t_utils.extend_table({print_log_level = "DEBUG"}, opts) + return Client(opts) +end describe("Client", function() it("should be instanciable", function() assert.has_no_errors(function() - local client = Client({contact_points = FAKE_CLUSTER}) + local client = client_factory({contact_points = FAKE_CLUSTER}) assert.equal(false, client.connected) end) end) describe("#execute()", function() it("should return error if no host is available", function() - local client = Client({contact_points = FAKE_CLUSTER}) + local client = client_factory({contact_points = FAKE_CLUSTER}) local err = client:execute() assert.truthy(err) assert.equal("NoHostAvailableError", err.type) end) it("should connect to a cluster", function() - pending() - local client = Client({contact_points = CLUSTER_2_1}) + local client = client_factory({contact_points = contact_points_2_1}) local err = client:execute() assert.falsy(err) end) it("should retrieve cluster information when connecting", function() - local client = Client({contact_points = CLUSTER_2_1}) + local client = client_factory({contact_points = contact_points_2_1}) local err = client:execute() assert.falsy(err) end) end) describe("binary protocol downgrade", function() it("should downgrade the protocol version if the node does not support the most recent one", function() - pending() - local client = Client({contact_points = CLUSTER_2_0}) + local client = client_factory({contact_points = contact_points_2_0}) local err = client:execute() assert.falsy(err) end) diff --git a/src/cassandra/client.lua b/src/cassandra/client.lua index e5f797f..e08ff0d 100644 --- a/src/cassandra/client.lua +++ b/src/cassandra/client.lua @@ -2,6 +2,7 @@ local Object = require "cassandra.classic" local client_options = require "cassandra.client_options" local ControlConnection = require "cassandra.control_connection" +local Logger = require "cassandra.logger" --- CLIENT -- @section client @@ -10,10 +11,14 @@ local Client = Object:extend() function Client:new(options) options = client_options.parse(options) + options.logger = Logger(options.print_log_level) + + self.options = options self.keyspace = options.keyspace self.hosts = {} self.connected = false - self.controlConnection = ControlConnection({contact_points = options.contact_points}) + + self.controlConnection = ControlConnection(options) end local function _connect(self) @@ -25,6 +30,9 @@ local function _connect(self) return err end + local inspect = require "inspect" + --print(inspect(self.hosts)) + self.connected = true end diff --git a/src/cassandra/client_options.lua b/src/cassandra/client_options.lua index 788b84a..eedae02 100644 --- a/src/cassandra/client_options.lua +++ b/src/cassandra/client_options.lua @@ -6,7 +6,14 @@ local errors = require "cassandra.errors" local DEFAULTS = { contact_points = {}, - keyspace = "" + keyspace = "", + print_log_level = "ERR", + policies = { + address_resolution = require "cassandra.policies.address_resolution" + }, + protocol_options = { + default_port = 9042 + } } local function parse(options) @@ -19,7 +26,7 @@ local function parse(options) end if not utils.is_array(options.contact_points) then - error("contact_points must be an array (integer-indexed table") + error("contact_points must be an array (integer-indexed table)") end if #options.contact_points < 1 then @@ -30,6 +37,9 @@ local function parse(options) error("keyspace must be a string") end + assert(type(options.protocol_options.default_port) == "number", "protocol default_port must be a number") + assert(type(options.policies.address_resolution) == "function", "address_resolution policy must be a function") + return options end diff --git a/src/cassandra/consts.lua b/src/cassandra/consts.lua index 5c2bdb3..d35c7a0 100644 --- a/src/cassandra/consts.lua +++ b/src/cassandra/consts.lua @@ -6,6 +6,5 @@ return { DEFAULT_PROTOCOL_VERSION = DEFAULT_PROTOCOL_VERSION, MIN_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSION[1], MAX_PROTOCOL_VERSION = DEFAULT_PROTOCOL_VERSION, - CQL_VERSION = "3.0.0", - DEFAULT_CQL_PORT = 9042 + CQL_VERSION = "3.0.0" } diff --git a/src/cassandra/control_connection.lua b/src/cassandra/control_connection.lua index 55bc750..83a4fb0 100644 --- a/src/cassandra/control_connection.lua +++ b/src/cassandra/control_connection.lua @@ -4,16 +4,14 @@ local Object = require "cassandra.classic" local Host = require "cassandra.host" local HostConnection = require "cassandra.host_connection" local RequestHandler = require "cassandra.request_handler" -local utils = require "cassandra.utils" -local log = require "cassandra.log" local requests = require "cassandra.requests" local table_insert = table.insert --- Constants -- @section constants -local SELECT_PEERS_QUERY = "SELECT peer,data_center,rack,tokens,rpc_address,release_version FROM system.peers" -local SELECT_LOCAL_QUERY = "SELECT * FROM system.local WHERE key='local'" +local SELECT_PEERS_QUERY = "SELECT peer,data_center,rack,rpc_address,release_version FROM system.peers" +local SELECT_LOCAL_QUERY = "SELECT data_center,rack,rpc_address,release_version FROM system.local WHERE key='local'" --- CONTROL_CONNECTION -- @section control_connection @@ -21,48 +19,98 @@ local SELECT_LOCAL_QUERY = "SELECT * FROM system.local WHERE key='local'" local ControlConnection = Object:extend() function ControlConnection:new(options) - -- @TODO check attributes are valid (contact points, etc...) self.hosts = {} - self.contact_points = options.contact_points + self.log = options.logger + self.options = options end function ControlConnection:init() - for _, contact_point in ipairs(self.contact_points) do + local contact_points = {} + for _, address in ipairs(self.options.contact_points) do -- Extract port if string is of the form "host:port" - local addr, port = utils.split_by_colon(contact_point) - if not port then port = CONSTS.DEFAULT_CQL_PORT end - table_insert(self.hosts, Host(addr, port)) + contact_points[address] = Host(address, self.options) end - local any_host, err = RequestHandler.get_first_host(self.hosts) + local err + + local host, err = RequestHandler.get_first_host(contact_points) if err then return nil, err end - local err = self:refresh_hosts(any_host) - - -- @TODO get peers info - -- @TODO get local info - -- local peers, err - -- local local_infos, err + err = self:refresh_hosts(host) + if err then + return nil, err + end return self.hosts end function ControlConnection:refresh_hosts(host) - log.debug("Refreshing local and peers info") - return self:get_peers(host) + self.log:info("Refreshing local and peers info") + local err + + err = self:get_local(host) + if err then + return err + end + + err = self:get_peers(host) + if err then + return err + end +end + +function ControlConnection:get_local(host) + local local_query = requests.QueryRequest(SELECT_LOCAL_QUERY) + local rows, err = host.connection:send(local_query) + if err then + return err + end + + local row = rows[1] + local local_host = self.hosts[host.address] + if not local_host then + local_host = Host(host.address, self.options) + end + + local_host.datacenter = row["data_center"] + local_host.rack = row["rack"] + local_host.cassandra_version = row["release_version"] + local_host.connection.protocol_version = host.connection.protocol_version + + self.hosts[host.address] = local_host + self.log:info("Local info retrieved") end function ControlConnection:get_peers(host) local peers_query = requests.QueryRequest(SELECT_PEERS_QUERY) - local result, err = host.connection:send(peers_query) + local rows, err = host.connection:send(peers_query) if err then return err end - local inspect = require "inspect" - print("Peers result: "..inspect(result)) + for _, row in ipairs(rows) do + local address = self.options.policies.address_resolution(row["rpc_address"]) + local new_host = self.hosts[address] + if new_host == nil then + new_host = Host(address, self.options) + self.log:info("Adding host "..new_host.address) + end + + new_host.datacenter = row["data_center"] + new_host.rack = row["rack"] + new_host.cassandra_version = row["release_version"] + new_host.connection.protocol_version = host.connection.protocol_version + + self.hosts[address] = new_host + end + + self.log:info("Peers info retrieved") +end + +function ControlConnection:add_hosts(rows, host_connection) + end return ControlConnection diff --git a/src/cassandra/frame_reader.lua b/src/cassandra/frame_reader.lua index 4860b0f..718609e 100644 --- a/src/cassandra/frame_reader.lua +++ b/src/cassandra/frame_reader.lua @@ -69,6 +69,7 @@ local function parse_metadata(buffer) local has_more_pages = bit.btest(flags, ROWS_RESULT_FLAGS.HAS_MORE_PAGES) local has_global_table_spec = bit.btest(flags, ROWS_RESULT_FLAGS.GLOBAL_TABLES_SPEC) + local has_no_metadata = bit.btest(flags, ROWS_RESULT_FLAGS.NO_METADATA) if has_global_table_spec then k_name = buffer:read_string() @@ -110,10 +111,10 @@ local RESULT_PARSERS = { for _ = 1, rows_count do local row = {} for i = 1, columns_count do - print("reading column "..columns[i].name) + --print("reading column "..columns[i].name) local value = buffer:read_cql_value(columns[i].type) local inspect = require "inspect" - print("column "..columns[i].name.." = "..inspect(value)) + --print("column "..columns[i].name.." = "..inspect(value)) row[columns[i].name] = value end rows[#rows + 1] = row @@ -157,8 +158,6 @@ function FrameReader:parse() error("frame header has no op_code") end - print("response op_code: "..op_code) - -- Parse frame depending on op_code if op_code == op_codes.ERROR then return nil, parse_error(self.frameBody) diff --git a/src/cassandra/host.lua b/src/cassandra/host.lua index 7533c2d..ff7d579 100644 --- a/src/cassandra/host.lua +++ b/src/cassandra/host.lua @@ -1,5 +1,6 @@ --- Represent one Cassandra node local Object = require "cassandra.classic" +local utils = require "cassandra.utils.string" local HostConnection = require "cassandra.host_connection" local string_find = string.find @@ -8,13 +9,18 @@ local string_find = string.find local Host = Object:extend() -function Host:new(address, port) - self.address = address..":"..port - self.casandra_version = nil +function Host:new(address, options) + local host, port = utils.split_by_colon(address) + if not port then port = options.protocol_options.default_port end + + self.address = address + + self.cassandra_version = nil self.datacenter = nil self.rack = nil + self.unhealthy_at = 0 - self.connection = HostConnection(address, port) + self.connection = HostConnection(host, port, {logger = options.logger}) end return Host diff --git a/src/cassandra/host_connection.lua b/src/cassandra/host_connection.lua index 9aec398..5718eb6 100644 --- a/src/cassandra/host_connection.lua +++ b/src/cassandra/host_connection.lua @@ -1,7 +1,6 @@ --- Represent one socket to connect to a Cassandra node local Object = require "cassandra.classic" local CONSTS = require "cassandra.consts" -local log = require "cassandra.log" local requests = require "cassandra.requests" local frame_header = require "cassandra.types.frame_header" local frame_reader = require "cassandra.frame_reader" @@ -48,16 +47,17 @@ end local HostConnection = Object:extend() -function HostConnection:new(address, port) +function HostConnection:new(address, port, options) self.address = address self.port = port self.protocol_version = CONSTS.DEFAULT_PROTOCOL_VERSION + self.log = options.logger end function HostConnection:decrease_version() self.protocol_version = self.protocol_version - 1 if self.protocol_version < CONSTS.MIN_PROTOCOL_VERSION then - error("minimum protocol version supported: ", CONSTS.MIN_PROTOCOL_VERSION) + error("minimum protocol version supported: "..CONSTS.MIN_PROTOCOL_VERSION) end end @@ -86,8 +86,6 @@ local function send_and_receive(self, request) end local frameHeader = FrameHeader.from_raw_bytes(frame_version_byte, header_bytes) - print("BODY BYTES: "..frameHeader.body_length) - print("OP_CODE: "..frameHeader.op_code) -- Receive frame body local body_bytes @@ -112,14 +110,14 @@ end function HostConnection:close() local res, err = self.socket:close() if err then - log.err("Could not close socket for connection to "..self.address..":"..self.port..". ", err) + self.log:err("Could not close socket for connection to "..self.address..":"..self.port..". ", err) end return res == 1 end --- Determine the protocol version to use and send the STARTUP request local function startup(self) - log.debug("Startup request. Trying to use protocol v"..self.protocol_version) + self.log:info("Startup request. Trying to use protocol v"..self.protocol_version) local startup_req = requests.StartupRequest() return self.send(self, startup_req) @@ -129,30 +127,30 @@ function HostConnection:open() local address = self.address..":"..self.port new_socket(self) - log.debug("Connecting to ", address) + self.log:info("Connecting to ".. address) local ok, err = self.socket:connect(self.address, self.port) if ok ~= 1 then - log.debug("Could not connect to "..address, err) + self.log:info("Could not connect to "..address..". "..err) return false, err end - log.debug("Socket connected to ", address) + self.log:info("Socket connected to ".. address) local res, err = startup(self) if err then - log.debug("Startup request failed. ", err) + self.log:info("Startup request failed. ", err) -- Check for incorrect protocol version if err and err.code == frame_reader.errors.PROTOCOL then if string_find(err.message, "Invalid or unsupported protocol version:", nil, true) then self:close() self:decrease_version() - log.debug("Decreasing protocol version to v"..self.protocol_version) + self.log:info("Decreasing protocol version to v"..self.protocol_version) return self:open() end end return false, err elseif res.ready then - log.debug("Host at "..address.." is ready with protocol v"..self.protocol_version) + self.log:info("Host at "..address.." is ready with protocol v"..self.protocol_version) return true end end diff --git a/src/cassandra/log.lua b/src/cassandra/log.lua deleted file mode 100644 index c06b76d..0000000 --- a/src/cassandra/log.lua +++ /dev/null @@ -1,29 +0,0 @@ -local string_format = string.format -local unpack = unpack -local type = type - -local LEVELS = { - "ERR", - "INFO", - "DEBUG" -} - -local _LOG = {} - -local function log(level, ...) - local arg = {...} - if ngx and type(ngx.log) == "function" then - -- lua-nginx-module - ngx.log(ngx[level], unpack(arg)) - else - print(string_format("%s: ", level), unpack(arg)) -- can't configure level for now - end -end - -for _, level in ipairs(LEVELS) do - _LOG[level:lower()] = function(...) - log(level, ...) - end -end - -return _LOG diff --git a/src/cassandra/logger.lua b/src/cassandra/logger.lua new file mode 100644 index 0000000..2d330cd --- /dev/null +++ b/src/cassandra/logger.lua @@ -0,0 +1,39 @@ +local string_format = string.format +local unpack = unpack +local type = type + +local LEVELS = { + ["ERR"] = 1, + ["INFO"] = 2, + ["DEBUG"] = 3 +} + +local function default_print_handler(self, level, level_name, ...) + local arg = {...} + if level <= self.print_lvl then + print(string_format("%s -- %s", level_name, unpack(arg))) + end +end + +local Log = {} +Log.__index = Log + +for log_level_name, log_level in pairs(LEVELS) do + Log[log_level_name:lower()] = function(self, ...) + if ngx and type(ngx.log) == "function" then + -- lua-nginx-module + ngx.log(ngx[log_level_name], ...) + else + self:print_handler(log_level, log_level_name:lower(), ...) + end + end +end + +function Log:__call(print_lvl, print_handler) + return setmetatable({ + print_lvl = print_lvl and LEVELS[print_lvl] or LEVELS.ERR, + print_handler = print_handler and print_handler or default_print_handler + }, Log) +end + +return setmetatable({}, Log) diff --git a/src/cassandra/policies/address_resolution.lua b/src/cassandra/policies/address_resolution.lua new file mode 100644 index 0000000..7f55e32 --- /dev/null +++ b/src/cassandra/policies/address_resolution.lua @@ -0,0 +1,9 @@ +local function translate(host, port) + if port then + return host..":"..port + else + return host + end +end + +return translate diff --git a/src/cassandra/request_handler.lua b/src/cassandra/request_handler.lua index d108534..0df3b91 100644 --- a/src/cassandra/request_handler.lua +++ b/src/cassandra/request_handler.lua @@ -17,7 +17,7 @@ end -- Get the first connection from the available one with no regards for the load balancing policy function RequestHandler.get_first_host(hosts) local errors = {} - for _, host in ipairs(hosts) do + for _, host in pairs(hosts) do local connected, err = host.connection:open() if not connected then errors[host.address] = err diff --git a/src/cassandra/types/frame_header.lua b/src/cassandra/types/frame_header.lua index 6be52c2..383c9d5 100644 --- a/src/cassandra/types/frame_header.lua +++ b/src/cassandra/types/frame_header.lua @@ -93,8 +93,6 @@ function FrameHeader.from_raw_bytes(version_byte, raw_bytes) local version = FrameHeader.version_from_byte(version_byte) local buffer = Buffer(version, raw_bytes) local flags = buffer:read_byte() - print("VERSION: "..version) - print("FLAGS: "..flags) local stream_id if version < 3 then diff --git a/src/cassandra/utils/table.lua b/src/cassandra/utils/table.lua index 084d6fd..11bcf5b 100644 --- a/src/cassandra/utils/table.lua +++ b/src/cassandra/utils/table.lua @@ -7,6 +7,9 @@ function _M.extend_table(defaults, values) if values[k] == nil then values[k] = defaults[k] end + if type(defaults[k]) == "table" then + _M.extend_table(defaults[k], values[k]) + end end end From 94cd00a4546eaada9c1e8a2b834613eb4760038d Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Thu, 5 Nov 2015 14:44:32 -0800 Subject: [PATCH 14/78] fix(rewrite) gave tests some love --- spec/integration/client_spec.lua | 25 +++++++++++++++++++++---- spec/unit/requests_spec.lua | 2 +- src/cassandra/client.lua | 2 +- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/spec/integration/client_spec.lua b/spec/integration/client_spec.lua index a6dd4da..ace8f17 100644 --- a/spec/integration/client_spec.lua +++ b/spec/integration/client_spec.lua @@ -2,8 +2,8 @@ local Client = require "cassandra.client" local t_utils = require "cassandra.utils.table" local FAKE_CLUSTER = {"0.0.0.1", "0.0.0.2", "0.0.0.3"} -local contact_points_2_0 = {"127.0.0.1:9001"} -local contact_points_2_1 = {"127.0.0.1:9101"} +--local contact_points_2_0 = {"127.0.0.1"} +local contact_points_2_1 = {"127.0.0.1"} local function client_factory(opts) t_utils.extend_table({print_log_level = "DEBUG"}, opts) @@ -23,23 +23,40 @@ describe("Client", function() local err = client:execute() assert.truthy(err) assert.equal("NoHostAvailableError", err.type) + assert.False(client.connected) end) it("should connect to a cluster", function() local client = client_factory({contact_points = contact_points_2_1}) local err = client:execute() assert.falsy(err) + assert.True(client.connected) end) it("should retrieve cluster information when connecting", function() local client = client_factory({contact_points = contact_points_2_1}) local err = client:execute() assert.falsy(err) + assert.True(client.connected) + + local hosts = client.hosts + assert.truthy(hosts["127.0.0.1"]) + assert.truthy(hosts["127.0.0.2"]) + assert.truthy(hosts["127.0.0.3"]) + + for _, host in pairs(hosts) do + assert.truthy(host.address) + assert.truthy(host.cassandra_version) + assert.truthy(host.rack) + assert.truthy(host.datacenter) + assert.truthy(host.connection.port) + assert.truthy(host.connection.protocol_version) + end end) - end) - describe("binary protocol downgrade", function() it("should downgrade the protocol version if the node does not support the most recent one", function() + pending() local client = client_factory({contact_points = contact_points_2_0}) local err = client:execute() assert.falsy(err) + assert.True(client.connected) end) end) end) diff --git a/spec/unit/requests_spec.lua b/spec/unit/requests_spec.lua index f83fd58..3c5548b 100644 --- a/spec/unit/requests_spec.lua +++ b/spec/unit/requests_spec.lua @@ -26,7 +26,7 @@ describe("Requests", function() local startup = requests.StartupRequest() startup:set_version(2) - local full_buffer = Buffer(3, startup:get_full_frame()) + local full_buffer = Buffer(2, startup:get_full_frame()) assert.equal(0x02, full_buffer:read_byte()) assert.equal(0, full_buffer:read_byte()) diff --git a/src/cassandra/client.lua b/src/cassandra/client.lua index e08ff0d..60237df 100644 --- a/src/cassandra/client.lua +++ b/src/cassandra/client.lua @@ -31,7 +31,7 @@ local function _connect(self) end local inspect = require "inspect" - --print(inspect(self.hosts)) + print(inspect(self.hosts)) self.connected = true end From e3cd9428de8adf093a1e8259e49098bd09384b08 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Thu, 5 Nov 2015 15:14:43 -0800 Subject: [PATCH 15/78] feat(rewrite) client shutdown --- spec/integration/client_spec.lua | 93 +++++++++++++++++----------- src/cassandra/client.lua | 25 +++++++- src/cassandra/control_connection.lua | 18 ++---- src/cassandra/host.lua | 4 ++ src/cassandra/host_connection.lua | 7 ++- 5 files changed, 92 insertions(+), 55 deletions(-) diff --git a/spec/integration/client_spec.lua b/spec/integration/client_spec.lua index ace8f17..1f78903 100644 --- a/spec/integration/client_spec.lua +++ b/spec/integration/client_spec.lua @@ -18,45 +18,64 @@ describe("Client", function() end) end) describe("#execute()", function() - it("should return error if no host is available", function() - local client = client_factory({contact_points = FAKE_CLUSTER}) - local err = client:execute() - assert.truthy(err) - assert.equal("NoHostAvailableError", err.type) - assert.False(client.connected) - end) - it("should connect to a cluster", function() - local client = client_factory({contact_points = contact_points_2_1}) - local err = client:execute() - assert.falsy(err) - assert.True(client.connected) + describe("#connect() on first #execute()", function() + local client + + after_each(function() + local err = client:shutdown() + assert.falsy(err) + end) + + it("should return error if no host is available", function() + client = client_factory({contact_points = FAKE_CLUSTER}) + local _, err = client:execute() + assert.truthy(err) + assert.equal("NoHostAvailableError", err.type) + assert.False(client.connected) + end) + it("should connect to a cluster", function() + client = client_factory({contact_points = contact_points_2_1}) + local _, err = client:execute() + assert.falsy(err) + assert.True(client.connected) + end) + it("should retrieve cluster information when connecting", function() + client = client_factory({contact_points = contact_points_2_1}) + local _, err = client:execute() + assert.falsy(err) + assert.True(client.connected) + + local hosts = client.hosts + assert.truthy(hosts["127.0.0.1"]) + assert.truthy(hosts["127.0.0.2"]) + assert.truthy(hosts["127.0.0.3"]) + + -- Contact point used should have a socket + assert.truthy(hosts[contact_points_2_1[1]].connection.socket) + + for _, host in pairs(hosts) do + assert.truthy(host.address) + assert.truthy(host.cassandra_version) + assert.truthy(host.rack) + assert.truthy(host.datacenter) + assert.truthy(host.connection.port) + assert.truthy(host.connection.protocol_version) + end + end) + it("should downgrade the protocol version if the node does not support the most recent one", function() + pending() + client = client_factory({contact_points = contact_points_2_0}) + local _, err = client:execute() + assert.falsy(err) + assert.True(client.connected) + end) end) - it("should retrieve cluster information when connecting", function() + describe("idk yet", function() local client = client_factory({contact_points = contact_points_2_1}) - local err = client:execute() - assert.falsy(err) - assert.True(client.connected) - - local hosts = client.hosts - assert.truthy(hosts["127.0.0.1"]) - assert.truthy(hosts["127.0.0.2"]) - assert.truthy(hosts["127.0.0.3"]) - - for _, host in pairs(hosts) do - assert.truthy(host.address) - assert.truthy(host.cassandra_version) - assert.truthy(host.rack) - assert.truthy(host.datacenter) - assert.truthy(host.connection.port) - assert.truthy(host.connection.protocol_version) - end - end) - it("should downgrade the protocol version if the node does not support the most recent one", function() - pending() - local client = client_factory({contact_points = contact_points_2_0}) - local err = client:execute() - assert.falsy(err) - assert.True(client.connected) + + it("should", function() + + end) end) end) end) diff --git a/src/cassandra/client.lua b/src/cassandra/client.lua index 60237df..62856ba 100644 --- a/src/cassandra/client.lua +++ b/src/cassandra/client.lua @@ -17,6 +17,7 @@ function Client:new(options) self.keyspace = options.keyspace self.hosts = {} self.connected = false + self.log = options.logger self.controlConnection = ControlConnection(options) end @@ -31,7 +32,7 @@ local function _connect(self) end local inspect = require "inspect" - print(inspect(self.hosts)) + --print(inspect(self.hosts)) self.connected = true end @@ -39,8 +40,28 @@ end function Client:execute() local err = _connect(self) if err then - return err + return nil, err end end +--- Close connection to the cluster. +-- Close all connections to all hosts and forget about them. +-- @return err An error from socket:close() if any, nil otherwise. +function Client:shutdown() + self.log:info("Shutting down") + if not self.connected or self.hosts == nil then + return + end + + for _, host in pairs(self.hosts) do + local closed, err = host:shutdown() + if not closed then + return err + end + end + + self.hosts = {} + self.connected = false +end + return Client diff --git a/src/cassandra/control_connection.lua b/src/cassandra/control_connection.lua index 83a4fb0..54348bb 100644 --- a/src/cassandra/control_connection.lua +++ b/src/cassandra/control_connection.lua @@ -69,17 +69,11 @@ function ControlConnection:get_local(host) end local row = rows[1] - local local_host = self.hosts[host.address] - if not local_host then - local_host = Host(host.address, self.options) - end - - local_host.datacenter = row["data_center"] - local_host.rack = row["rack"] - local_host.cassandra_version = row["release_version"] - local_host.connection.protocol_version = host.connection.protocol_version + host.datacenter = row["data_center"] + host.rack = row["rack"] + host.cassandra_version = row["release_version"] - self.hosts[host.address] = local_host + self.hosts[host.address] = host self.log:info("Local info retrieved") end @@ -109,8 +103,4 @@ function ControlConnection:get_peers(host) self.log:info("Peers info retrieved") end -function ControlConnection:add_hosts(rows, host_connection) - -end - return ControlConnection diff --git a/src/cassandra/host.lua b/src/cassandra/host.lua index ff7d579..0a9dcc0 100644 --- a/src/cassandra/host.lua +++ b/src/cassandra/host.lua @@ -23,4 +23,8 @@ function Host:new(address, options) self.connection = HostConnection(host, port, {logger = options.logger}) end +function Host:shutdown() + return self.connection:close() +end + return Host diff --git a/src/cassandra/host_connection.lua b/src/cassandra/host_connection.lua index 5718eb6..babd320 100644 --- a/src/cassandra/host_connection.lua +++ b/src/cassandra/host_connection.lua @@ -108,11 +108,14 @@ function HostConnection:send(request) end function HostConnection:close() + if self.socket == nil then return true end + + self.log:debug("Closing connection to "..self.address..":"..self.port..".") local res, err = self.socket:close() if err then - self.log:err("Could not close socket for connection to "..self.address..":"..self.port..". ", err) + self.log:err("Could not close socket for connection to "..self.address..":"..self.port..". "..err) end - return res == 1 + return res == 1, err end --- Determine the protocol version to use and send the STARTUP request From e3d1f0068348e11e85e9cd3a75806fd56242e82f Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Thu, 5 Nov 2015 17:28:16 -0800 Subject: [PATCH 16/78] feat(rewrite) execute() and load balancing policies Default load balancing policy is RoundRoubin. --- spec/integration/client_spec.lua | 108 ++++++++++++---------- src/cassandra/client.lua | 12 ++- src/cassandra/client_options.lua | 3 +- src/cassandra/host_connection.lua | 35 +++---- src/cassandra/policies/load_balancing.lua | 39 ++++++++ src/cassandra/request_handler.lua | 42 +++++++-- 6 files changed, 166 insertions(+), 73 deletions(-) create mode 100644 src/cassandra/policies/load_balancing.lua diff --git a/spec/integration/client_spec.lua b/spec/integration/client_spec.lua index 1f78903..21f1d3f 100644 --- a/spec/integration/client_spec.lua +++ b/spec/integration/client_spec.lua @@ -17,64 +17,76 @@ describe("Client", function() assert.equal(false, client.connected) end) end) - describe("#execute()", function() - describe("#connect() on first #execute()", function() - local client + describe("#_connect()", function() + local client + + after_each(function() + local err = client:shutdown() + assert.falsy(err) + end) + + it("should return error if no host is available", function() + client = client_factory({contact_points = FAKE_CLUSTER}) + local err = client:_connect() + assert.truthy(err) + assert.equal("NoHostAvailableError", err.type) + assert.False(client.connected) + end) + it("should connect to a cluster", function() + client = client_factory({contact_points = contact_points_2_1}) + local err = client:_connect() + assert.falsy(err) + assert.True(client.connected) + end) + it("should retrieve cluster information when connecting", function() + client = client_factory({contact_points = contact_points_2_1}) + local err = client:_connect() + assert.falsy(err) + assert.True(client.connected) + + local hosts = client.hosts + assert.truthy(hosts["127.0.0.1"]) + assert.truthy(hosts["127.0.0.2"]) + assert.truthy(hosts["127.0.0.3"]) + + -- Contact point used should have a socket + assert.truthy(hosts[contact_points_2_1[1]].connection.socket) + + for _, host in pairs(hosts) do + assert.truthy(host.address) + assert.truthy(host.cassandra_version) + assert.truthy(host.rack) + assert.truthy(host.datacenter) + assert.truthy(host.connection.port) + assert.truthy(host.connection.protocol_version) + end + end) + it("should downgrade the protocol version if the node does not support the most recent one", function() + pending() + client = client_factory({contact_points = contact_points_2_0}) + local err = client:_connect() + assert.falsy(err) + assert.True(client.connected) + end) + describe("#execute()", function() + local client = client_factory({contact_points = contact_points_2_1}) after_each(function() local err = client:shutdown() assert.falsy(err) end) - it("should return error if no host is available", function() - client = client_factory({contact_points = FAKE_CLUSTER}) - local _, err = client:execute() - assert.truthy(err) - assert.equal("NoHostAvailableError", err.type) - assert.False(client.connected) - end) - it("should connect to a cluster", function() - client = client_factory({contact_points = contact_points_2_1}) - local _, err = client:execute() - assert.falsy(err) - assert.True(client.connected) - end) - it("should retrieve cluster information when connecting", function() - client = client_factory({contact_points = contact_points_2_1}) - local _, err = client:execute() - assert.falsy(err) - assert.True(client.connected) + it("should send a request through the configured load balancer", function() + spy.on(client.options.policies.load_balancing, "iterator") - local hosts = client.hosts - assert.truthy(hosts["127.0.0.1"]) - assert.truthy(hosts["127.0.0.2"]) - assert.truthy(hosts["127.0.0.3"]) + local res, err = client:execute("SELECT peer FROM system.peers") - -- Contact point used should have a socket - assert.truthy(hosts[contact_points_2_1[1]].connection.socket) + assert.spy(client.options.policies.load_balancing.iterator).was.called() - for _, host in pairs(hosts) do - assert.truthy(host.address) - assert.truthy(host.cassandra_version) - assert.truthy(host.rack) - assert.truthy(host.datacenter) - assert.truthy(host.connection.port) - assert.truthy(host.connection.protocol_version) - end - end) - it("should downgrade the protocol version if the node does not support the most recent one", function() - pending() - client = client_factory({contact_points = contact_points_2_0}) - local _, err = client:execute() assert.falsy(err) - assert.True(client.connected) - end) - end) - describe("idk yet", function() - local client = client_factory({contact_points = contact_points_2_1}) - - it("should", function() - + assert.truthy(res) + assert.equal("ROWS", res.type) + assert.equal(2, #res) end) end) end) diff --git a/src/cassandra/client.lua b/src/cassandra/client.lua index 62856ba..854a8f9 100644 --- a/src/cassandra/client.lua +++ b/src/cassandra/client.lua @@ -3,6 +3,8 @@ local Object = require "cassandra.classic" local client_options = require "cassandra.client_options" local ControlConnection = require "cassandra.control_connection" local Logger = require "cassandra.logger" +local Requests = require "cassandra.requests" +local RequestHandler = require "cassandra.request_handler" --- CLIENT -- @section client @@ -31,17 +33,23 @@ local function _connect(self) return err end - local inspect = require "inspect" + --local inspect = require "inspect" --print(inspect(self.hosts)) self.connected = true end -function Client:execute() +Client._connect = _connect + +function Client:execute(query) local err = _connect(self) if err then return nil, err end + + local query_request = Requests.QueryRequest(query) + local handler = RequestHandler(query_request, self.hosts, self.options) + return handler:send() end --- Close connection to the cluster. diff --git a/src/cassandra/client_options.lua b/src/cassandra/client_options.lua index eedae02..a94dee4 100644 --- a/src/cassandra/client_options.lua +++ b/src/cassandra/client_options.lua @@ -9,7 +9,8 @@ local DEFAULTS = { keyspace = "", print_log_level = "ERR", policies = { - address_resolution = require "cassandra.policies.address_resolution" + address_resolution = require "cassandra.policies.address_resolution", + load_balancing = require("cassandra.policies.load_balancing").RoundRobin() }, protocol_options = { default_port = 9042 diff --git a/src/cassandra/host_connection.lua b/src/cassandra/host_connection.lua index babd320..d6b56cd 100644 --- a/src/cassandra/host_connection.lua +++ b/src/cassandra/host_connection.lua @@ -47,18 +47,17 @@ end local HostConnection = Object:extend() -function HostConnection:new(address, port, options) - self.address = address +function HostConnection:new(host, port, options) + self.host = host self.port = port + self.address = host..":"..port self.protocol_version = CONSTS.DEFAULT_PROTOCOL_VERSION self.log = options.logger + self.connected = false end function HostConnection:decrease_version() self.protocol_version = self.protocol_version - 1 - if self.protocol_version < CONSTS.MIN_PROTOCOL_VERSION then - error("minimum protocol version supported: "..CONSTS.MIN_PROTOCOL_VERSION) - end end --- Socket operations @@ -110,10 +109,10 @@ end function HostConnection:close() if self.socket == nil then return true end - self.log:debug("Closing connection to "..self.address..":"..self.port..".") + self.log:debug("Closing connection to "..self.address..".") local res, err = self.socket:close() if err then - self.log:err("Could not close socket for connection to "..self.address..":"..self.port..". "..err) + self.log:err("Could not close socket for connection to "..self.address..". "..err) end return res == 1, err end @@ -127,33 +126,37 @@ local function startup(self) end function HostConnection:open() - local address = self.address..":"..self.port new_socket(self) - self.log:info("Connecting to ".. address) - local ok, err = self.socket:connect(self.address, self.port) + self.log:info("Connecting to "..self.address) + local ok, err = self.socket:connect(self.host, self.port) if ok ~= 1 then - self.log:info("Could not connect to "..address..". "..err) + self.log:info("Could not connect to "..self.address..". "..err) return false, err end - self.log:info("Socket connected to ".. address) + self.log:info("Socket connected to "..self.address) local res, err = startup(self) if err then - self.log:info("Startup request failed. ", err) + self.log:info("Startup request failed. "..err) -- Check for incorrect protocol version if err and err.code == frame_reader.errors.PROTOCOL then if string_find(err.message, "Invalid or unsupported protocol version:", nil, true) then self:close() self:decrease_version() - self.log:info("Decreasing protocol version to v"..self.protocol_version) - return self:open() + if self.protocol_version < CONSTS.MIN_PROTOCOL_VERSION then + self.log:err("Connection could not find a supported protocol version.") + else + self.log:info("Decreasing protocol version to v"..self.protocol_version) + return self:open() + end end end return false, err elseif res.ready then - self.log:info("Host at "..address.." is ready with protocol v"..self.protocol_version) + self.connected = true + self.log:info("Host at "..self.address.." is ready with protocol v"..self.protocol_version) return true end end diff --git a/src/cassandra/policies/load_balancing.lua b/src/cassandra/policies/load_balancing.lua new file mode 100644 index 0000000..e8b80d9 --- /dev/null +++ b/src/cassandra/policies/load_balancing.lua @@ -0,0 +1,39 @@ +local math_fmod = math.fmod + +local RoundRobin = { + new = function(self) + self.index = 0 + end, + iterator = function(self) + -- return an iterator to be used + return function(hosts) + local keys = {} + for k in pairs(hosts) do + keys[#keys + 1] = k + end + + local n = #keys + local counter = 0 + local plan_index = math_fmod(self.index, n) + self.index = self.index + 1 + + return function(t, i) + local mod = math_fmod(plan_index, n) + 1 + + plan_index = plan_index + 1 + counter = counter + 1 + + if counter <= n then + return mod, hosts[keys[mod]] + end + end + end + end +} + +return { + RoundRobin = function() + RoundRobin:new() + return RoundRobin + end +} diff --git a/src/cassandra/request_handler.lua b/src/cassandra/request_handler.lua index 0df3b91..a1c9315 100644 --- a/src/cassandra/request_handler.lua +++ b/src/cassandra/request_handler.lua @@ -1,17 +1,47 @@ local Object = require "cassandra.classic" local Errors = require "cassandra.errors" -local ipairs = ipairs --- RequestHandler -- @section request_handler local RequestHandler = Object:extend() -function RequestHandler:mew(options) - self.loadBalancingPolicy = nil -- @TODO - self.retryPolicy = nil -- @TODO - self.request = options.request - self.host = options.host +function RequestHandler:new(request, hosts, client_options) + self.request = request + self.hosts = hosts + + self.connection = nil + self.load_balancing_policy = client_options.policies.load_balancing + self.retry_policy = nil -- @TODO + self.log = client_options.logger +end + +function RequestHandler:get_next_connection() + local errors = {} + local iter = self.load_balancing_policy:iterator() + + for _, host in iter(self.hosts) do + local connected, err = host.connection:open() + if connected then + return host.connection + else + -- @TODO Mark host as down + errors[host.address] = err + end + end + + return nil, Errors.NoHostAvailableError(errors) +end + +function RequestHandler:send() + local connection, err = self:get_next_connection() + if not connection then + return nil, err + end + + self.log:info("Acquired connection through load balancing policy: "..connection.address) + + return connection:send(self.request) end -- Get the first connection from the available one with no regards for the load balancing policy From 19b151ec4202c7adadc8176d02fdd07b7fdecd05 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Tue, 10 Nov 2015 14:10:00 -0800 Subject: [PATCH 17/78] feat(rewrite) request_handler takes node status in account --- spec/load.lua | 14 +++++++++++ spec/unit/host_spec.lua | 28 +++++++++++++++++++++ src/cassandra/host.lua | 42 ++++++++++++++++++++++++++++--- src/cassandra/host_connection.lua | 12 ++++++--- src/cassandra/logger.lua | 5 ++-- src/cassandra/request_handler.lua | 16 +++++++----- src/cassandra/utils/time.lua | 13 ++++++++++ 7 files changed, 114 insertions(+), 16 deletions(-) create mode 100644 spec/load.lua create mode 100644 spec/unit/host_spec.lua create mode 100644 src/cassandra/utils/time.lua diff --git a/spec/load.lua b/spec/load.lua new file mode 100644 index 0000000..3c3c634 --- /dev/null +++ b/spec/load.lua @@ -0,0 +1,14 @@ +package.path = package.path..";src/?.lua" +local Client = require "cassandra.client" + +local client = Client({contact_points = {"127.0.0.1", "127.0.0.2"}, print_log_level = "INFO"}) + +for i = 1, 10000 do + local res, err = client:execute("SELECT peer FROM system.peers") + if err then + error(err) + end + print("Request "..i.." successful.") +end + +client:shutdown() diff --git a/spec/unit/host_spec.lua b/spec/unit/host_spec.lua new file mode 100644 index 0000000..87c2da9 --- /dev/null +++ b/spec/unit/host_spec.lua @@ -0,0 +1,28 @@ +local Host = require "cassandra.host" + +local opts = { + logger = { + warn = function()end, + info = function()end + } +} + +describe("Host", function() + local host + it("should be instanciable", function() + host = Host("127.0.0.1:9042", opts) + host.reconnection_delay = 0 + assert.equal(0, host.unhealthy_at) + assert.True(host:can_be_considered_up()) + end) + it("should be possible to mark it as DOWN", function() + host:set_down() + assert.equal(os.time() * 1000, host.unhealthy_at) + assert.False(host:can_be_considered_up()) + end) + it("should be possible to mark as UP", function() + host:set_up() + assert.equal(0, host.unhealthy_at) + assert.True(host:can_be_considered_up()) + end) +end) diff --git a/src/cassandra/host.lua b/src/cassandra/host.lua index 0a9dcc0..2150db9 100644 --- a/src/cassandra/host.lua +++ b/src/cassandra/host.lua @@ -1,8 +1,8 @@ --- Represent one Cassandra node local Object = require "cassandra.classic" -local utils = require "cassandra.utils.string" +local time_utils = require "cassandra.utils.time" +local string_utils = require "cassandra.utils.string" local HostConnection = require "cassandra.host_connection" -local string_find = string.find --- Host -- @section host @@ -10,19 +10,53 @@ local string_find = string.find local Host = Object:extend() function Host:new(address, options) - local host, port = utils.split_by_colon(address) + local host, port = string_utils.split_by_colon(address) if not port then port = options.protocol_options.default_port end self.address = address - self.cassandra_version = nil self.datacenter = nil self.rack = nil self.unhealthy_at = 0 + self.reconnection_delay = 5 -- seconds + + self.log = options.logger self.connection = HostConnection(host, port, {logger = options.logger}) end +function Host:set_down() + if not self:is_up() then + return + end + self.log:warn("Setting host "..self.address.." as DOWN") + self.unhealthy_at = time_utils.get_time() + self:shutdown() +end + +function Host:set_up() + if self:is_up() then + return + end + self.log:info("Setting host "..self.address.." as UP") + self.unhealthy_at = 0 +end + +function Host:is_up() + return self.unhealthy_at == 0 +end + +function Host:can_be_considered_up() + return self:is_up() or (time_utils.get_time() - self.unhealthy_at > self.reconnection_delay) +end + +function Host:open() + if not self:is_up() then + self.log:err("RETRYING OPENING "..self.address) + end + return self.connection:open() +end + function Host:shutdown() return self.connection:close() end diff --git a/src/cassandra/host_connection.lua b/src/cassandra/host_connection.lua index d6b56cd..ba9f6f6 100644 --- a/src/cassandra/host_connection.lua +++ b/src/cassandra/host_connection.lua @@ -100,7 +100,6 @@ local function send_and_receive(self, request) return frameReader:parse() end - function HostConnection:send(request) request:set_version(self.protocol_version) return send_and_receive(self, request) @@ -109,12 +108,15 @@ end function HostConnection:close() if self.socket == nil then return true end - self.log:debug("Closing connection to "..self.address..".") + self.log:info("Closing connection to "..self.address..".") local res, err = self.socket:close() - if err then + if res ~= 1 then self.log:err("Could not close socket for connection to "..self.address..". "..err) + return false, err + else + self.connected = false + return true end - return res == 1, err end --- Determine the protocol version to use and send the STARTUP request @@ -126,6 +128,8 @@ local function startup(self) end function HostConnection:open() + if self.connected then return true end + new_socket(self) self.log:info("Connecting to "..self.address) diff --git a/src/cassandra/logger.lua b/src/cassandra/logger.lua index 2d330cd..35eb8b4 100644 --- a/src/cassandra/logger.lua +++ b/src/cassandra/logger.lua @@ -4,8 +4,9 @@ local type = type local LEVELS = { ["ERR"] = 1, - ["INFO"] = 2, - ["DEBUG"] = 3 + ["WARN"] = 2, + ["INFO"] = 3, + ["DEBUG"] = 4 } local function default_print_handler(self, level, level_name, ...) diff --git a/src/cassandra/request_handler.lua b/src/cassandra/request_handler.lua index a1c9315..a9e675b 100644 --- a/src/cassandra/request_handler.lua +++ b/src/cassandra/request_handler.lua @@ -21,12 +21,16 @@ function RequestHandler:get_next_connection() local iter = self.load_balancing_policy:iterator() for _, host in iter(self.hosts) do - local connected, err = host.connection:open() - if connected then - return host.connection + if host:can_be_considered_up() then + local connected, err = host:open() + if connected then + return host.connection + else + host:set_down() + errors[host.address] = err + end else - -- @TODO Mark host as down - errors[host.address] = err + errors[host.address] = "Host considered DOWN" end end @@ -48,7 +52,7 @@ end function RequestHandler.get_first_host(hosts) local errors = {} for _, host in pairs(hosts) do - local connected, err = host.connection:open() + local connected, err = host:open() if not connected then errors[host.address] = err else diff --git a/src/cassandra/utils/time.lua b/src/cassandra/utils/time.lua new file mode 100644 index 0000000..3500861 --- /dev/null +++ b/src/cassandra/utils/time.lua @@ -0,0 +1,13 @@ +local type = type + +local function get_time() + if ngx and type(ngx.now) == "function" then + return ngx.now() * 1000 + else + return os.time() * 1000 + end +end + +return { + get_time = get_time +} From 2e684e042011e25d4b58972a1a3e2754f1151e16 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Wed, 11 Nov 2015 18:55:19 -0800 Subject: [PATCH 18/78] refactor(rewrite) lua-resty style --- .gitignore | 1 + src/cassandra.lua | 392 ++++++++++++++++++++++ src/cassandra/client.lua | 75 ----- src/cassandra/client_options.lua | 43 ++- src/cassandra/control_connection.lua | 106 ------ src/cassandra/errors.lua | 12 + src/cassandra/frame_reader.lua | 2 +- src/cassandra/host.lua | 64 ---- src/cassandra/host_connection.lua | 77 +++-- src/cassandra/log.lua | 15 + src/cassandra/logger.lua | 40 --- src/cassandra/policies/load_balancing.lua | 48 +-- src/cassandra/request_handler.lua | 66 ---- src/cassandra/storage.lua | 162 +++++++++ 14 files changed, 687 insertions(+), 416 deletions(-) create mode 100644 .gitignore create mode 100644 src/cassandra.lua delete mode 100644 src/cassandra/client.lua delete mode 100644 src/cassandra/control_connection.lua delete mode 100644 src/cassandra/host.lua create mode 100644 src/cassandra/log.lua delete mode 100644 src/cassandra/logger.lua delete mode 100644 src/cassandra/request_handler.lua create mode 100644 src/cassandra/storage.lua diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..de069ae --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +nginx_tmp diff --git a/src/cassandra.lua b/src/cassandra.lua new file mode 100644 index 0000000..ef3e3ee --- /dev/null +++ b/src/cassandra.lua @@ -0,0 +1,392 @@ +local Object = require "cassandra.classic" +local CONSTS = require "cassandra.consts" +local Errors = require "cassandra.errors" +local Requests = require "cassandra.requests" +local storage = require "cassandra.storage" +local frame_header = require "cassandra.types.frame_header" +local frame_reader = require "cassandra.frame_reader" +local client_options = require "cassandra.client_options" +local string_utils = require "cassandra.utils.string" +local log = require "cassandra.log" + +local table_insert = table.insert +local string_find = string.find + +local FrameReader = frame_reader.FrameReader +local FrameHeader = frame_header.FrameHeader + +--- Host +-- A connection to a single host. +-- Not cluster aware, only maintain a socket to its peer. +-- @section host + +local Host = Object:extend() + +local function new_socket(self) + local tcp_sock, sock_type + + if ngx and ngx.get_phase ~= nil and ngx.get_phase() ~= "init" then + -- lua-nginx-module + tcp_sock = ngx.socket.tcp + sock_type = "ngx" + else + -- fallback to luasocket + tcp_sock = require("socket").tcp + sock_type = "luasocket" + end + + local socket, err = tcp_sock() + if not socket then + error(err) + end + + self.socket = socket + self.socket_type = sock_type +end + +function Host:new(address, options) + local host, port = string_utils.split_by_colon(address) + if not port then port = options.protocol_options.default_port end + + self.host = host + self.port = port + self.address = host..":"..port + self.protocol_version = CONSTS.DEFAULT_PROTOCOL_VERSION + + self.options = options + + new_socket(self) +end + +local function send_and_receive(self, request) + -- Send frame + local bytes_sent, err = self.socket:send(request:get_full_frame()) + if bytes_sent == nil then + return nil, err + end + + -- Receive frame version byte + local frame_version_byte, err = self.socket:receive(1) + if frame_version_byte == nil then + return nil, err + end + + local n_bytes_to_receive = FrameHeader.size_from_byte(frame_version_byte) - 1 + + -- Receive frame header + local header_bytes, err = self.socket:receive(n_bytes_to_receive) + if header_bytes == nil then + return nil, err + end + + local frameHeader = FrameHeader.from_raw_bytes(frame_version_byte, header_bytes) + + -- Receive frame body + local body_bytes + if frameHeader.body_length > 0 then + body_bytes, err = self.socket:receive(frameHeader.body_length) + if body_bytes == nil then + return nil, err + end + end + + return FrameReader(frameHeader, body_bytes) +end + +function Host:send(request) + request:set_version(self.protocol_version) + + --self:set_timeout(self.socket_options.read_timeout) + + local frameReader, err = send_and_receive(self, request) + if err then + if err == "timeout" then + return nil, Errors.TimeoutError(self.address) + else + return nil, Errors.SocketError(self.address, err) + end + end + + -- result, cql_error + return frameReader:parse() +end + +local function startup(self) + log.info("Startup request. Trying to use protocol v"..self.protocol_version) + + local startup_req = Requests.StartupRequest() + return self:send(startup_req) +end + +function Host:connect() + log.info("Connecting to "..self.address) + + local ok, err = self.socket:connect(self.host, self.port) + if ok ~= 1 then + log.info("Could not connect to "..self.address..". Reason: "..err) + return false, err + end + + log.info("Session connected to "..self.address) + + if self:get_reused_times() > 0 then + return true + end + + -- Startup request on first connection + local res, err = startup(self) + if err then + log.info("Startup request failed. "..err) + -- Check for incorrect protocol version + if err and err.code == frame_reader.errors.PROTOCOL then + if string_find(err.message, "Invalid or unsupported protocol version:", nil, true) then + self:close() + self:decrease_version() + if self.protocol_version < CONSTS.MIN_PROTOCOL_VERSION then + log.err("Connection could not find a supported protocol version.") + else + log.info("Decreasing protocol version to v"..self.protocol_version) + return self:connect() + end + end + end + + return false, err + elseif res.ready then + log.info("Host at "..self.address.." is ready with protocol v"..self.protocol_version) + return true + end +end + +function Host:get_reused_times() + if self.socket_type == "ngx" then + local count, err = self.socket:getreusedtimes() + if err then + log.err("Could not get reused times for socket to "..self.addres..". "..err) + end + return count + end + + -- luasocket + return 0 +end + +function Host:set_keep_alive() + if self.socket_type == "ngx" then + local ok, err = self.socket:setkeepalive() + if err then + log.err("Could not set keepalive for socket to "..self.addres..". "..err) + end + return ok + end + + return true +end + +function Host:close() + log.info("Closing connection to "..self.address..".") + local res, err = self.socket:close() + if res ~= 1 then + log.err("Could not close socket for connection to "..self.address..". "..err) + return false, err + else + return true + end +end + +--- Request handler +-- @section request_handler + +local RequestHandler = Object:extend() + +function RequestHandler.get_first_host(hosts) + local errors = {} + for _, host in ipairs(hosts) do + local connected, err = host:connect() + if not connected then + errors[host.address] = err + else + return host + end + end + + return nil, Errors.NoHostAvailableError(errors) +end + +--- Session +-- An expandable session, cluster-aware through the storage cache. +-- Uses a load balancing policy to select nodes on which to perform requests. +-- @section session + +local Session = {} + +function Session:new(options) + options = client_options.parse_session(options) + + local s = { + options = options + } + + return setmetatable(s, {__index = self}) +end + +function Session:get_next_connection() + local errors = {} + + local iter = self.options.policies.load_balancing + local hosts = storage.get_hosts(options.shm) + + for _, addr in iter(self.options.shm, hosts) do + if storage.can_host_be_considered_up(self.options.shm, addr) then + local host_infos = storage.get_host(self.options.shm, addr) + local host = Host(addr, self.options) + local connected, err = host:connect() + if connected then + return host + else + errors[addr] = err + end + else + errors[addr] = "Host considered DOWN" + end + end + + return nil, Errors.NoHostAvailableError(errors) +end + +function Session:execute(query) + local host, err = self:get_next_connection() + if err then + return nil, err + end + + log.info("Acquired connection through load balancing policy: "..host.address) + + local query_request = Requests.QueryRequest(query) + local result, err = host:send(query_request) + if err then + return nil, err + end + + -- Success! Make sure to re-up node in case it was marked as DOWN + storage.set_host_up(self.options.shm, host.host) + + if host.socket_type == "ngx" then + host:set_keep_alive() + else + host:close() + end + + return result +end + +function Session:handle_error(err) + if err.type == "SocketError" then + -- host seems unhealthy + self.host:set_down() + -- always retry + elseif err.type == "TimeoutError" then + -- on timeout + elseif err.type == "ResponseError" then + if err.code == CQL_Errors.OVERLOADED or err.code == CQL_Errors.IS_BOOTSTRAPPING or err.code == CQL_Errors.TRUNCATE_ERROR then + -- always retry + elseif err.code == CQL_Errors.UNAVAILABLE_EXCEPTION then + -- make retry decision based on retry_policy on_unavailable + elseif err.code == CQL_Errors.READ_TIMEOUT then + -- make retry decision based on retry_policy read_timeout + elseif err.code == CQL_Errors.WRITE_TIMEOUT then + -- make retry decision based on retry_policy write_timeout + end + end + + -- this error needs to be reported to the client + return nil, err +end + +--- Cassandra +-- @section cassandra + +local Cassandra = { + _VERSION = "0.4.0" +} + +function Cassandra.spawn_session(options) + return Session:new(options) +end + +local SELECT_PEERS_QUERY = "SELECT peer,data_center,rack,rpc_address,release_version FROM system.peers" +local SELECT_LOCAL_QUERY = "SELECT data_center,rack,rpc_address,release_version FROM system.local WHERE key='local'" + +--- Retrieve cluster informations form a connected contact_point +function Cassandra.refresh_hosts(contact_points_hosts, options) + log.info("Refreshing local and peers info") + local host, err = RequestHandler.get_first_host(contact_points_hosts) + local local_query = Requests.QueryRequest(SELECT_LOCAL_QUERY) + local peers_query = Requests.QueryRequest(SELECT_PEERS_QUERY) + local hosts = {} + + local rows, err = host:send(local_query) + if err then + return false, err + end + local row = rows[1] + local address = options.policies.address_resolution(row["rpc_address"]) + local local_host = { + datacenter = row["data_center"], + rack = row["rack"], + cassandra_version = row["release_version"], + protocol_versiom = row["native_protocol_version"], + unhealthy_at = 0, + reconnection_delay = 5 + } + hosts[address] = local_host + log.info("Local info retrieved") + + rows, err = host:send(peers_query) + if err then + return false, err + end + + for _, row in ipairs(rows) do + address = options.policies.address_resolution(row["rpc_address"]) + log.info("Adding host "..address) + hosts[address] = { + datacenter = row["data_center"], + rack = row["rack"], + cassandra_version = row["release_version"], + protocol_version = local_host.native_protocol_version, + unhealthy_at = 0, + reconnection_delay = 5 + } + end + log.info("Peers info retrieved") + + -- Store cluster mapping for future sessions + local addresses = {} + for addr, host in pairs(hosts) do + table_insert(addresses, addr) + storage.set_host(options.shm, addr, host) + end + storage.set_hosts(options.shm, addresses) + + return true +end + +--- Retrieve cluster informations and store them in ngx.shared.DICT +function Cassandra.spawn_cluster(options) + options = client_options.parse_cluster(options) + + local contact_points_hosts = {} + for _, contact_point in ipairs(options.contact_points) do + table_insert(contact_points_hosts, Host(contact_point, options)) + end + + local ok, err = Cassandra.refresh_hosts(contact_points_hosts, options) + if not ok then + return false, err + end + + return true +end + +return Cassandra diff --git a/src/cassandra/client.lua b/src/cassandra/client.lua deleted file mode 100644 index 854a8f9..0000000 --- a/src/cassandra/client.lua +++ /dev/null @@ -1,75 +0,0 @@ ---- Responsible for a cluster of nodes -local Object = require "cassandra.classic" -local client_options = require "cassandra.client_options" -local ControlConnection = require "cassandra.control_connection" -local Logger = require "cassandra.logger" -local Requests = require "cassandra.requests" -local RequestHandler = require "cassandra.request_handler" - ---- CLIENT --- @section client - -local Client = Object:extend() - -function Client:new(options) - options = client_options.parse(options) - options.logger = Logger(options.print_log_level) - - self.options = options - self.keyspace = options.keyspace - self.hosts = {} - self.connected = false - self.log = options.logger - - self.controlConnection = ControlConnection(options) -end - -local function _connect(self) - if self.connected then return end - - local err - self.hosts, err = self.controlConnection:init() - if err then - return err - end - - --local inspect = require "inspect" - --print(inspect(self.hosts)) - - self.connected = true -end - -Client._connect = _connect - -function Client:execute(query) - local err = _connect(self) - if err then - return nil, err - end - - local query_request = Requests.QueryRequest(query) - local handler = RequestHandler(query_request, self.hosts, self.options) - return handler:send() -end - ---- Close connection to the cluster. --- Close all connections to all hosts and forget about them. --- @return err An error from socket:close() if any, nil otherwise. -function Client:shutdown() - self.log:info("Shutting down") - if not self.connected or self.hosts == nil then - return - end - - for _, host in pairs(self.hosts) do - local closed, err = host:shutdown() - if not closed then - return err - end - end - - self.hosts = {} - self.connected = false -end - -return Client diff --git a/src/cassandra/client_options.lua b/src/cassandra/client_options.lua index a94dee4..e6eb20b 100644 --- a/src/cassandra/client_options.lua +++ b/src/cassandra/client_options.lua @@ -1,25 +1,44 @@ local utils = require "cassandra.utils.table" -local errors = require "cassandra.errors" ---- CONST --- @section constants +--- Defaults +-- @section defaults local DEFAULTS = { + shm = "cassandra", contact_points = {}, - keyspace = "", - print_log_level = "ERR", policies = { address_resolution = require "cassandra.policies.address_resolution", - load_balancing = require("cassandra.policies.load_balancing").RoundRobin() + load_balancing = require("cassandra.policies.load_balancing").RoundRobin }, protocol_options = { default_port = 9042 + }, + socket_options = { + connect_timeout = 5000, + read_timeout = 12000 } } -local function parse(options) +local function parse_session(cassandra) + if options == nil then options = {} end + + utils.extend_table(DEFAULTS, options) + + --if type(options.keyspace) ~= "string" then + --error("keyspace must be a string") + --end + + assert(type(options.protocol_options.default_port) == "number", "protocol default_port must be a number") + assert(type(options.policies.address_resolution) == "function", "address_resolution policy must be a function") + + return options +end + +local function parse_cluster(options) if options == nil then options = {} end + parse_session(options) + utils.extend_table(DEFAULTS, options) if type(options.contact_points) ~= "table" then @@ -34,16 +53,10 @@ local function parse(options) error("contact_points must contain at least one contact point") end - if type(options.keyspace) ~= "string" then - error("keyspace must be a string") - end - - assert(type(options.protocol_options.default_port) == "number", "protocol default_port must be a number") - assert(type(options.policies.address_resolution) == "function", "address_resolution policy must be a function") - return options end return { - parse = parse + parse_cluster = parse_cluster, + parse_session = parse_session } diff --git a/src/cassandra/control_connection.lua b/src/cassandra/control_connection.lua deleted file mode 100644 index 54348bb..0000000 --- a/src/cassandra/control_connection.lua +++ /dev/null @@ -1,106 +0,0 @@ ---- Represent a connection from the driver to the cluster and handle events between the two -local CONSTS = require "cassandra.consts" -local Object = require "cassandra.classic" -local Host = require "cassandra.host" -local HostConnection = require "cassandra.host_connection" -local RequestHandler = require "cassandra.request_handler" -local requests = require "cassandra.requests" -local table_insert = table.insert - ---- Constants --- @section constants - -local SELECT_PEERS_QUERY = "SELECT peer,data_center,rack,rpc_address,release_version FROM system.peers" -local SELECT_LOCAL_QUERY = "SELECT data_center,rack,rpc_address,release_version FROM system.local WHERE key='local'" - ---- CONTROL_CONNECTION --- @section control_connection - -local ControlConnection = Object:extend() - -function ControlConnection:new(options) - self.hosts = {} - self.log = options.logger - self.options = options -end - -function ControlConnection:init() - local contact_points = {} - for _, address in ipairs(self.options.contact_points) do - -- Extract port if string is of the form "host:port" - contact_points[address] = Host(address, self.options) - end - - local err - - local host, err = RequestHandler.get_first_host(contact_points) - if err then - return nil, err - end - - err = self:refresh_hosts(host) - if err then - return nil, err - end - - return self.hosts -end - -function ControlConnection:refresh_hosts(host) - self.log:info("Refreshing local and peers info") - local err - - err = self:get_local(host) - if err then - return err - end - - err = self:get_peers(host) - if err then - return err - end -end - -function ControlConnection:get_local(host) - local local_query = requests.QueryRequest(SELECT_LOCAL_QUERY) - local rows, err = host.connection:send(local_query) - if err then - return err - end - - local row = rows[1] - host.datacenter = row["data_center"] - host.rack = row["rack"] - host.cassandra_version = row["release_version"] - - self.hosts[host.address] = host - self.log:info("Local info retrieved") -end - -function ControlConnection:get_peers(host) - local peers_query = requests.QueryRequest(SELECT_PEERS_QUERY) - local rows, err = host.connection:send(peers_query) - if err then - return err - end - - for _, row in ipairs(rows) do - local address = self.options.policies.address_resolution(row["rpc_address"]) - local new_host = self.hosts[address] - if new_host == nil then - new_host = Host(address, self.options) - self.log:info("Adding host "..new_host.address) - end - - new_host.datacenter = row["data_center"] - new_host.rack = row["rack"] - new_host.cassandra_version = row["release_version"] - new_host.connection.protocol_version = host.connection.protocol_version - - self.hosts[address] = new_host - end - - self.log:info("Peers info retrieved") -end - -return ControlConnection diff --git a/src/cassandra/errors.lua b/src/cassandra/errors.lua index c1af08e..459b074 100644 --- a/src/cassandra/errors.lua +++ b/src/cassandra/errors.lua @@ -28,6 +28,18 @@ local ERROR_TYPES = { meta = function(code) return {code = code} end + }, + SocketError = { + info = "Represents a client-side error that is raised when a socket returns an error from one of its operations.", + message = function(address, message) + return message.." for socket with peer "..address + end + }, + TimeoutError = { + info = "Represents a client-side error that is raised when the client didn't hear back from the server within {client_options.socket_options.read_timeout}.", + message = function(address) + return "timeout for peer "..address + end } } diff --git a/src/cassandra/frame_reader.lua b/src/cassandra/frame_reader.lua index 718609e..6b6b455 100644 --- a/src/cassandra/frame_reader.lua +++ b/src/cassandra/frame_reader.lua @@ -113,7 +113,7 @@ local RESULT_PARSERS = { for i = 1, columns_count do --print("reading column "..columns[i].name) local value = buffer:read_cql_value(columns[i].type) - local inspect = require "inspect" + --local inspect = require "inspect" --print("column "..columns[i].name.." = "..inspect(value)) row[columns[i].name] = value end diff --git a/src/cassandra/host.lua b/src/cassandra/host.lua deleted file mode 100644 index 2150db9..0000000 --- a/src/cassandra/host.lua +++ /dev/null @@ -1,64 +0,0 @@ ---- Represent one Cassandra node -local Object = require "cassandra.classic" -local time_utils = require "cassandra.utils.time" -local string_utils = require "cassandra.utils.string" -local HostConnection = require "cassandra.host_connection" - ---- Host --- @section host - -local Host = Object:extend() - -function Host:new(address, options) - local host, port = string_utils.split_by_colon(address) - if not port then port = options.protocol_options.default_port end - - self.address = address - self.cassandra_version = nil - self.datacenter = nil - self.rack = nil - - self.unhealthy_at = 0 - self.reconnection_delay = 5 -- seconds - - self.log = options.logger - self.connection = HostConnection(host, port, {logger = options.logger}) -end - -function Host:set_down() - if not self:is_up() then - return - end - self.log:warn("Setting host "..self.address.." as DOWN") - self.unhealthy_at = time_utils.get_time() - self:shutdown() -end - -function Host:set_up() - if self:is_up() then - return - end - self.log:info("Setting host "..self.address.." as UP") - self.unhealthy_at = 0 -end - -function Host:is_up() - return self.unhealthy_at == 0 -end - -function Host:can_be_considered_up() - return self:is_up() or (time_utils.get_time() - self.unhealthy_at > self.reconnection_delay) -end - -function Host:open() - if not self:is_up() then - self.log:err("RETRYING OPENING "..self.address) - end - return self.connection:open() -end - -function Host:shutdown() - return self.connection:close() -end - -return Host diff --git a/src/cassandra/host_connection.lua b/src/cassandra/host_connection.lua index ba9f6f6..7eb75bf 100644 --- a/src/cassandra/host_connection.lua +++ b/src/cassandra/host_connection.lua @@ -1,5 +1,6 @@ --- Represent one socket to connect to a Cassandra node local Object = require "cassandra.classic" +local Errors = require "cassandra.errors" local CONSTS = require "cassandra.consts" local requests = require "cassandra.requests" local frame_header = require "cassandra.types.frame_header" @@ -52,8 +53,12 @@ function HostConnection:new(host, port, options) self.port = port self.address = host..":"..port self.protocol_version = CONSTS.DEFAULT_PROTOCOL_VERSION + --self.connected = false + self.log = options.logger - self.connected = false + self.socket_options = options.socket_options + + new_socket(self) end function HostConnection:decrease_version() @@ -63,6 +68,36 @@ end --- Socket operations -- @section socket +function HostConnection:get_reused_times() + if self.socket_type == SOCKET_TYPES.NGX then + return self.socket:getreusedtimes() + end + + -- luasocket + return 0 +end + +function HostConnection:close() + self.log:info("Closing connection to "..self.address..".") + local res, err = self.socket:close() + if res ~= 1 then + self.log:err("Could not close socket for connection to "..self.address..". "..err) + return false, err + else + --self.connected = false + return true + end +end + +function HostConnection:set_timeout(timeout) + if self.socket_type == SOCKET_TYPES.LUASOCKET then + -- value is in seconds + timeout = timeout / 1000 + end + + self.socket:settimeout(timeout) +end + local function send_and_receive(self, request) -- Send frame local bytes_sent, err = self.socket:send(request:get_full_frame()) @@ -95,28 +130,27 @@ local function send_and_receive(self, request) end end - local frameReader = FrameReader(frameHeader, body_bytes) - - return frameReader:parse() + return FrameReader(frameHeader, body_bytes) end function HostConnection:send(request) request:set_version(self.protocol_version) - return send_and_receive(self, request) -end -function HostConnection:close() - if self.socket == nil then return true end + self:set_timeout(self.socket_options.read_timeout) - self.log:info("Closing connection to "..self.address..".") - local res, err = self.socket:close() - if res ~= 1 then - self.log:err("Could not close socket for connection to "..self.address..". "..err) - return false, err - else - self.connected = false - return true + local frameReader, err = send_and_receive(self, request) + if err then + if err == "timeout" then + return nil, Errors.TimeoutError(self.address) + else + return nil, Errors.SocketError(self.address, err) + end end + + --self:close() + + -- result, cql_error + return frameReader:parse() end --- Determine the protocol version to use and send the STARTUP request @@ -128,9 +162,9 @@ local function startup(self) end function HostConnection:open() - if self.connected then return true end + --if self.connected then return true end - new_socket(self) + self:set_timeout(self.socket_options.connect_timeout) self.log:info("Connecting to "..self.address) local ok, err = self.socket:connect(self.host, self.port) @@ -140,6 +174,11 @@ function HostConnection:open() end self.log:info("Socket connected to "..self.address) + -- Startup request if this socket has never been connected to it + if self:get_reused_times() > 0 then + return true + end + local res, err = startup(self) if err then self.log:info("Startup request failed. "..err) @@ -159,7 +198,7 @@ function HostConnection:open() return false, err elseif res.ready then - self.connected = true + --self.connected = true self.log:info("Host at "..self.address.." is ready with protocol v"..self.protocol_version) return true end diff --git a/src/cassandra/log.lua b/src/cassandra/log.lua new file mode 100644 index 0000000..79ae6a5 --- /dev/null +++ b/src/cassandra/log.lua @@ -0,0 +1,15 @@ +local ngx_log = ngx.log + +local log = {} + +for _, lvl in ipairs({"ERR", "WARN", "INFO", "DEBUG"}) do + log[lvl:lower()] = function(...) + if ngx ~= nil and ngx.get_phase() ~= "init" then + ngx_log(ngx[lvl], ...) + else + print(...) + end + end +end + +return log diff --git a/src/cassandra/logger.lua b/src/cassandra/logger.lua deleted file mode 100644 index 35eb8b4..0000000 --- a/src/cassandra/logger.lua +++ /dev/null @@ -1,40 +0,0 @@ -local string_format = string.format -local unpack = unpack -local type = type - -local LEVELS = { - ["ERR"] = 1, - ["WARN"] = 2, - ["INFO"] = 3, - ["DEBUG"] = 4 -} - -local function default_print_handler(self, level, level_name, ...) - local arg = {...} - if level <= self.print_lvl then - print(string_format("%s -- %s", level_name, unpack(arg))) - end -end - -local Log = {} -Log.__index = Log - -for log_level_name, log_level in pairs(LEVELS) do - Log[log_level_name:lower()] = function(self, ...) - if ngx and type(ngx.log) == "function" then - -- lua-nginx-module - ngx.log(ngx[log_level_name], ...) - else - self:print_handler(log_level, log_level_name:lower(), ...) - end - end -end - -function Log:__call(print_lvl, print_handler) - return setmetatable({ - print_lvl = print_lvl and LEVELS[print_lvl] or LEVELS.ERR, - print_handler = print_handler and print_handler or default_print_handler - }, Log) -end - -return setmetatable({}, Log) diff --git a/src/cassandra/policies/load_balancing.lua b/src/cassandra/policies/load_balancing.lua index e8b80d9..699846e 100644 --- a/src/cassandra/policies/load_balancing.lua +++ b/src/cassandra/policies/load_balancing.lua @@ -1,39 +1,27 @@ +local storage = require "cassandra.storage" local math_fmod = math.fmod +local pairs = pairs -local RoundRobin = { - new = function(self) - self.index = 0 - end, - iterator = function(self) - -- return an iterator to be used - return function(hosts) - local keys = {} - for k in pairs(hosts) do - keys[#keys + 1] = k - end - - local n = #keys - local counter = 0 - local plan_index = math_fmod(self.index, n) - self.index = self.index + 1 +return { + RoundRobin = function(shm, hosts) + local n = #hosts + local counter = 0 - return function(t, i) - local mod = math_fmod(plan_index, n) + 1 + local dict = storage.get_dict(shm) + local plan_index = dict:get("plan_index") + if not plan_index then + dict:set("plan_index", 0) + end - plan_index = plan_index + 1 - counter = counter + 1 + return function(t, i) + local plan_index = dict:get("plan_index") + local mod = math_fmod(plan_index, n) + 1 + dict:incr("plan_index", 1) + counter = counter + 1 - if counter <= n then - return mod, hosts[keys[mod]] - end + if counter <= n then + return mod, hosts[mod] end end end } - -return { - RoundRobin = function() - RoundRobin:new() - return RoundRobin - end -} diff --git a/src/cassandra/request_handler.lua b/src/cassandra/request_handler.lua deleted file mode 100644 index a9e675b..0000000 --- a/src/cassandra/request_handler.lua +++ /dev/null @@ -1,66 +0,0 @@ -local Object = require "cassandra.classic" -local Errors = require "cassandra.errors" - ---- RequestHandler --- @section request_handler - -local RequestHandler = Object:extend() - -function RequestHandler:new(request, hosts, client_options) - self.request = request - self.hosts = hosts - - self.connection = nil - self.load_balancing_policy = client_options.policies.load_balancing - self.retry_policy = nil -- @TODO - self.log = client_options.logger -end - -function RequestHandler:get_next_connection() - local errors = {} - local iter = self.load_balancing_policy:iterator() - - for _, host in iter(self.hosts) do - if host:can_be_considered_up() then - local connected, err = host:open() - if connected then - return host.connection - else - host:set_down() - errors[host.address] = err - end - else - errors[host.address] = "Host considered DOWN" - end - end - - return nil, Errors.NoHostAvailableError(errors) -end - -function RequestHandler:send() - local connection, err = self:get_next_connection() - if not connection then - return nil, err - end - - self.log:info("Acquired connection through load balancing policy: "..connection.address) - - return connection:send(self.request) -end - --- Get the first connection from the available one with no regards for the load balancing policy -function RequestHandler.get_first_host(hosts) - local errors = {} - for _, host in pairs(hosts) do - local connected, err = host:open() - if not connected then - errors[host.address] = err - else - return host - end - end - - return nil, Errors.NoHostAvailableError(errors) -end - -return RequestHandler diff --git a/src/cassandra/storage.lua b/src/cassandra/storage.lua new file mode 100644 index 0000000..f3fe441 --- /dev/null +++ b/src/cassandra/storage.lua @@ -0,0 +1,162 @@ +local json = require "cjson" +local log = require "cassandra.log" +local time_utils = require "cassandra.utils.time" +local string_format = string.format +local string_gsub = string.gsub +local table_concat = table.concat +local in_ngx = ngx ~= nil +local shared + +-- DICT Proxy +-- https://github.com/bsm/fakengx/blob/master/fakengx.lua + +local SharedDict = {} + +function SharedDict:new() + return setmetatable({data = {}}, {__index = self}) +end + +function SharedDict:get(key) + return self.data[key], 0 +end + +function SharedDict:set(key, value) + self.data[key] = value + return true, nil, false +end + +function SharedDict:add(key, value) + if self.data[key] ~= nil then + return false, "exists", false + end + + self.data[key] = value + return true, nil, false +end + +function SharedDict:replace(key, value) + if self.data[key] == nil then + return false, "not found", false + end + + self.data[key] = value + return true, nil, false +end + +function SharedDict:delete(key) + self.data[key] = nil +end + +function SharedDict:incr(key, value) + if not self.data[key] then + return nil, "not found" + elseif type(self.data[key]) ~= "number" then + return nil, "not a number" + end + + self.data[key] = self.data[key] + value + return self.data[key], nil +end + +if in_ngx then + shared = ngx.shared +else + shared = {} +end + +local function split(str, sep) + local sep, fields = sep or ":", {} + local pattern = string_format("([^%s]+)", sep) + string_gsub(str, pattern, function(c) fields[#fields+1] = c end) + return fields +end + +local function get_dict(shm) + if not in_ngx then + if shared[shm] == nil then + shared[shm] = SharedDict:new() + end + end + + return shared[shm] +end + +--- Hosts +-- @section hosts + +local _HOSTS_KEY = "hosts" +local _SEP = ";" + +local function set_hosts(shm, hosts) + local dict = get_dict(shm) + local ok, err = dict:set(_HOSTS_KEY, table_concat(hosts, _SEP)) + if not ok then + log.err("Cannot store hosts: "..err) + end +end + +local function get_hosts(shm) + local dict = get_dict(shm) + local value, err = dict:get(_HOSTS_KEY) + if err then + log.err("Cannot retrieve hosts: "..err) + end + return split(value, _SEP) +end + +--- Host +-- @section host + +local function set_host(shm, host_addr, host) + local dict = get_dict(shm) + local ok, err = dict:set(host_addr, json.encode(host)) + if not ok then + log.err("Cannot store hosts: "..err) + end +end + +local function get_host(shm, host_addr) + local dict = get_dict(shm) + local value, err = dict:get(host_addr) + if err then + log.err("Cannot retrieve host: "..err) + elseif value then + return json.decode(value) + end +end + +local function set_host_down(shm, host_addr) + log.warn("Setting host "..host_addr.." as DOWN") + local host = get_host(shm, host_addr) + host.unhealthy_at = time_utils.get_time() + set_host(shm, host_addr, host) +end + +local function set_host_up(shm, host_addr) + log.info("Setting host "..host_addr.." as UP") + local host = get_host(shm, host_addr) + host.unhealthy_at = 0 + set_host(shm, host_addr, host) +end + +local function is_host_up(shm, host_addr) + local host = get_host(shm, host_addr) + return host.unhealthy_at == 0 +end + +local function can_host_be_considered_up(shm, host_addr) + local host = get_host(shm, host_addr) + return is_host_up(shm, host_addr) or (time_utils.get_time() - host.unhealthy_at > host.reconnection_delay) +end + +return { + get_dict = get_dict, + get_host = get_host, + set_host = set_host, + set_hosts = set_hosts, + get_hosts = get_hosts, + set_host_up = set_host_up, + set_host_down = set_host_down, + is_host_up = is_host_up, + can_host_be_considered_up = can_host_be_considered_up +} From 20b3d4eee2ea4f7a0a34546724c0244bcca180a6 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Wed, 11 Nov 2015 20:43:26 -0800 Subject: [PATCH 19/78] rewrite: cleanup and moving split to utils --- .luacheckrc | 3 +- docker-compose.yml | 47 ----------------------- spec/integration/client_spec.lua | 2 +- spec/unit/requests_spec.lua | 1 - src/cassandra.lua | 12 ++++-- src/cassandra/client_options.lua | 2 +- src/cassandra/policies/load_balancing.lua | 1 - src/cassandra/storage.lua | 12 +----- src/cassandra/utils/buffer.lua | 2 - src/cassandra/utils/string.lua | 10 +++++ 10 files changed, 24 insertions(+), 68 deletions(-) delete mode 100644 docker-compose.yml diff --git a/.luacheckrc b/.luacheckrc index 65f5edb..68c5912 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -1,2 +1,3 @@ unused_args = false -globals = {"ngx", "describe", "setup", "teardown", "it", "pending"} +redefined = false +globals = {"ngx", "describe", "setup", "teardown", "it", "pending", "after_each", "spy"} diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 4357cb4..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,47 +0,0 @@ -2.0_cass_1: - image: cassandra:2.0 - container_name: 2.0_cass_1 - ports: - - "9001:9042" -2.0_cass_2: - image: cassandra:2.0 - container_name: 2.0_cass_2 - links: - - 2.0_cass_1 - environment: - - CASSANDRA_SEEDS=2.0_cass_1 - ports: - - "9002:9042" -2.0_cass_3: - image: cassandra:2.0 - container_name: 2.0_cass_3 - links: - - 2.0_cass_1 - environment: - - CASSANDRA_SEEDS=2.0_cass_1 - ports: - - "9003:9042" - -2.1_cass_1: - image: cassandra:2.1 - container_name: 2.1_cass_1 - ports: - - "9101:9042" -2.1_cass_2: - image: cassandra:2.1 - container_name: 2.1_cass_2 - links: - - 2.1_cass_1 - environment: - - CASSANDRA_SEEDS=2.1_cass_1 - ports: - - "9102:9042" -2.1_cass_3: - image: cassandra:2.1 - container_name: 2.1_cass_3 - links: - - 2.1_cass_1 - environment: - - CASSANDRA_SEEDS=2.1_cass_1 - ports: - - "9103:9042" diff --git a/spec/integration/client_spec.lua b/spec/integration/client_spec.lua index 21f1d3f..6f06e43 100644 --- a/spec/integration/client_spec.lua +++ b/spec/integration/client_spec.lua @@ -63,7 +63,7 @@ describe("Client", function() end) it("should downgrade the protocol version if the node does not support the most recent one", function() pending() - client = client_factory({contact_points = contact_points_2_0}) + --client = client_factory({contact_points = contact_points_2_0}) local err = client:_connect() assert.falsy(err) assert.True(client.connected) diff --git a/spec/unit/requests_spec.lua b/spec/unit/requests_spec.lua index 3c5548b..8db9ab2 100644 --- a/spec/unit/requests_spec.lua +++ b/spec/unit/requests_spec.lua @@ -1,4 +1,3 @@ -local CONSTS = require "cassandra.consts" local requests = require "cassandra.requests" local Buffer = require "cassandra.buffer" local frame_header = require "cassandra.types.frame_header" diff --git a/src/cassandra.lua b/src/cassandra.lua index ef3e3ee..0f78d80 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -162,7 +162,7 @@ function Host:get_reused_times() if self.socket_type == "ngx" then local count, err = self.socket:getreusedtimes() if err then - log.err("Could not get reused times for socket to "..self.addres..". "..err) + log.err("Could not get reused times for socket to "..self.address..". "..err) end return count end @@ -175,7 +175,7 @@ function Host:set_keep_alive() if self.socket_type == "ngx" then local ok, err = self.socket:setkeepalive() if err then - log.err("Could not set keepalive for socket to "..self.addres..". "..err) + log.err("Could not set keepalive for socket to "..self.address..". "..err) end return ok end @@ -234,11 +234,10 @@ function Session:get_next_connection() local errors = {} local iter = self.options.policies.load_balancing - local hosts = storage.get_hosts(options.shm) + local hosts = storage.get_hosts(self.options.shm) for _, addr in iter(self.options.shm, hosts) do if storage.can_host_be_considered_up(self.options.shm, addr) then - local host_infos = storage.get_host(self.options.shm, addr) local host = Host(addr, self.options) local connected, err = host:connect() if connected then @@ -320,7 +319,12 @@ local SELECT_LOCAL_QUERY = "SELECT data_center,rack,rpc_address,release_version --- Retrieve cluster informations form a connected contact_point function Cassandra.refresh_hosts(contact_points_hosts, options) log.info("Refreshing local and peers info") + local host, err = RequestHandler.get_first_host(contact_points_hosts) + if err then + return nil, err + end + local local_query = Requests.QueryRequest(SELECT_LOCAL_QUERY) local peers_query = Requests.QueryRequest(SELECT_PEERS_QUERY) local hosts = {} diff --git a/src/cassandra/client_options.lua b/src/cassandra/client_options.lua index e6eb20b..65fcd65 100644 --- a/src/cassandra/client_options.lua +++ b/src/cassandra/client_options.lua @@ -19,7 +19,7 @@ local DEFAULTS = { } } -local function parse_session(cassandra) +local function parse_session(options) if options == nil then options = {} end utils.extend_table(DEFAULTS, options) diff --git a/src/cassandra/policies/load_balancing.lua b/src/cassandra/policies/load_balancing.lua index 699846e..b391439 100644 --- a/src/cassandra/policies/load_balancing.lua +++ b/src/cassandra/policies/load_balancing.lua @@ -1,6 +1,5 @@ local storage = require "cassandra.storage" local math_fmod = math.fmod -local pairs = pairs return { RoundRobin = function(shm, hosts) diff --git a/src/cassandra/storage.lua b/src/cassandra/storage.lua index f3fe441..23db54a 100644 --- a/src/cassandra/storage.lua +++ b/src/cassandra/storage.lua @@ -1,8 +1,7 @@ local json = require "cjson" local log = require "cassandra.log" +local string_utils = require "cassandra.utils.string" local time_utils = require "cassandra.utils.time" -local string_format = string.format -local string_gsub = string.gsub local table_concat = table.concat local in_ngx = ngx ~= nil local shared @@ -64,13 +63,6 @@ else shared = {} end -local function split(str, sep) - local sep, fields = sep or ":", {} - local pattern = string_format("([^%s]+)", sep) - string_gsub(str, pattern, function(c) fields[#fields+1] = c end) - return fields -end - local function get_dict(shm) if not in_ngx then if shared[shm] == nil then @@ -101,7 +93,7 @@ local function get_hosts(shm) if err then log.err("Cannot retrieve hosts: "..err) end - return split(value, _SEP) + return string_utils.split(value, _SEP) end --- Host diff --git a/src/cassandra/utils/buffer.lua b/src/cassandra/utils/buffer.lua index b3235e3..303a4d5 100644 --- a/src/cassandra/utils/buffer.lua +++ b/src/cassandra/utils/buffer.lua @@ -1,7 +1,5 @@ local Object = require "cassandra.classic" local string_sub = string.sub -local table_insert = table.insert -local table_concat = table.concat local Buffer = Object:extend() diff --git a/src/cassandra/utils/string.lua b/src/cassandra/utils/string.lua index d1bcf2d..b1b10aa 100644 --- a/src/cassandra/utils/string.lua +++ b/src/cassandra/utils/string.lua @@ -1,3 +1,6 @@ +local string_format = string.format +local string_gsub = string.gsub + local _M = {} function _M.split_by_colon(str) @@ -6,4 +9,11 @@ function _M.split_by_colon(str) return fields[1], fields[2] end +function _M.split(str, sep) + local sep, fields = sep or ":", {} + local pattern = string_format("([^%s]+)", sep) + string_gsub(str, pattern, function(c) fields[#fields+1] = c end) + return fields +end + return _M From 31721491a716f16e0de2fce83a9c3442d342bf96 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Thu, 12 Nov 2015 14:20:20 -0800 Subject: [PATCH 20/78] cleanup(rewrite) catch cache errors and renamings --- spec/unit/cql_types_buffer_spec.lua | 2 +- src/cassandra.lua | 45 ++-- src/cassandra/{storage.lua => cache.lua} | 54 +++-- src/cassandra/constants.lua | 10 + src/cassandra/consts.lua | 10 - src/cassandra/host_connection.lua | 207 ------------------ .../{client_options.lua => options.lua} | 2 - src/cassandra/policies/load_balancing.lua | 11 +- src/cassandra/requests.lua | 4 +- src/cassandra/utils/table.lua | 2 +- 10 files changed, 87 insertions(+), 260 deletions(-) rename src/cassandra/{storage.lua => cache.lua} (71%) create mode 100644 src/cassandra/constants.lua delete mode 100644 src/cassandra/consts.lua delete mode 100644 src/cassandra/host_connection.lua rename src/cassandra/{client_options.lua => options.lua} (97%) diff --git a/spec/unit/cql_types_buffer_spec.lua b/spec/unit/cql_types_buffer_spec.lua index 4ebb190..2e8f017 100644 --- a/spec/unit/cql_types_buffer_spec.lua +++ b/spec/unit/cql_types_buffer_spec.lua @@ -2,7 +2,7 @@ local Buffer = require "cassandra.buffer" local CONSTS = require "cassandra.consts" local CQL_TYPES = require "cassandra.types.cql_types" -for _, protocol_version in ipairs(CONSTS.SUPPORTED_PROTOCOL_VERSION) do +for _, protocol_version in ipairs(CONSTS.SUPPORTED_PROTOCOL_VERSIONS) do describe("CQL Types protocol v"..protocol_version, function() local FIXTURES = { diff --git a/src/cassandra.lua b/src/cassandra.lua index 0f78d80..a3c23d2 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -1,11 +1,11 @@ local Object = require "cassandra.classic" -local CONSTS = require "cassandra.consts" +local CONSTS = require "cassandra.constants" local Errors = require "cassandra.errors" local Requests = require "cassandra.requests" -local storage = require "cassandra.storage" +local cache = require "cassandra.cache" local frame_header = require "cassandra.types.frame_header" local frame_reader = require "cassandra.frame_reader" -local client_options = require "cassandra.client_options" +local opts = require "cassandra.options" local string_utils = require "cassandra.utils.string" local log = require "cassandra.log" @@ -214,14 +214,14 @@ function RequestHandler.get_first_host(hosts) end --- Session --- An expandable session, cluster-aware through the storage cache. +-- An expandable session, cluster-aware through the cache. -- Uses a load balancing policy to select nodes on which to perform requests. -- @section session local Session = {} function Session:new(options) - options = client_options.parse_session(options) + options = opts.parse_session(options) local s = { options = options @@ -234,10 +234,17 @@ function Session:get_next_connection() local errors = {} local iter = self.options.policies.load_balancing - local hosts = storage.get_hosts(self.options.shm) + local hosts, err = cache.get_hosts(self.options.shm) + if err then + return nil, err + end for _, addr in iter(self.options.shm, hosts) do - if storage.can_host_be_considered_up(self.options.shm, addr) then + local can_host_be_considered_up, err = cache.can_host_be_considered_up(self.options.shm, addr) + if err then + return nil, err + end + if can_host_be_considered_up then local host = Host(addr, self.options) local connected, err = host:connect() if connected then @@ -268,7 +275,10 @@ function Session:execute(query) end -- Success! Make sure to re-up node in case it was marked as DOWN - storage.set_host_up(self.options.shm, host.host) + local ok, err = cache.set_host_up(self.options.shm, host.host) + if err then + return nil, err + end if host.socket_type == "ngx" then host:set_keep_alive() @@ -322,7 +332,7 @@ function Cassandra.refresh_hosts(contact_points_hosts, options) local host, err = RequestHandler.get_first_host(contact_points_hosts) if err then - return nil, err + return false, err end local local_query = Requests.QueryRequest(SELECT_LOCAL_QUERY) @@ -369,28 +379,25 @@ function Cassandra.refresh_hosts(contact_points_hosts, options) local addresses = {} for addr, host in pairs(hosts) do table_insert(addresses, addr) - storage.set_host(options.shm, addr, host) + local ok, err = cache.set_host(options.shm, addr, host) + if err then + return false, err + end end - storage.set_hosts(options.shm, addresses) - return true + return cache.set_hosts(options.shm, addresses) end --- Retrieve cluster informations and store them in ngx.shared.DICT function Cassandra.spawn_cluster(options) - options = client_options.parse_cluster(options) + options = opts.parse_cluster(options) local contact_points_hosts = {} for _, contact_point in ipairs(options.contact_points) do table_insert(contact_points_hosts, Host(contact_point, options)) end - local ok, err = Cassandra.refresh_hosts(contact_points_hosts, options) - if not ok then - return false, err - end - - return true + return Cassandra.refresh_hosts(contact_points_hosts, options) end return Cassandra diff --git a/src/cassandra/storage.lua b/src/cassandra/cache.lua similarity index 71% rename from src/cassandra/storage.lua rename to src/cassandra/cache.lua index 23db54a..e487183 100644 --- a/src/cassandra/storage.lua +++ b/src/cassandra/cache.lua @@ -83,16 +83,20 @@ local function set_hosts(shm, hosts) local dict = get_dict(shm) local ok, err = dict:set(_HOSTS_KEY, table_concat(hosts, _SEP)) if not ok then - log.err("Cannot store hosts: "..err) + err = "Cannot store hosts for cluster under shm "..shm..": "..err end + return ok, err end local function get_hosts(shm) local dict = get_dict(shm) local value, err = dict:get(_HOSTS_KEY) if err then - log.err("Cannot retrieve hosts: "..err) + return nil, "Cannot retrieve hosts for cluster under shm "..shm..": "..err + elseif value == nil then + return nil, "Not hosts set for cluster under "..shm end + return string_utils.split(value, _SEP) end @@ -103,42 +107,66 @@ local function set_host(shm, host_addr, host) local dict = get_dict(shm) local ok, err = dict:set(host_addr, json.encode(host)) if not ok then - log.err("Cannot store hosts: "..err) + err = "Cannot store host details for cluster "..shm..": "..err end + return ok, err end local function get_host(shm, host_addr) local dict = get_dict(shm) local value, err = dict:get(host_addr) if err then - log.err("Cannot retrieve host: "..err) - elseif value then - return json.decode(value) + return nil, "Cannot retrieve host details for cluster under shm "..shm..": "..err + elseif value == nil then + return nil, "No details for host "..host_addr.." under shm "..shm end + return json.decode(value) end local function set_host_down(shm, host_addr) log.warn("Setting host "..host_addr.." as DOWN") - local host = get_host(shm, host_addr) + local host, err = get_host(shm, host_addr) + if err then + return false, err + end + host.unhealthy_at = time_utils.get_time() - set_host(shm, host_addr, host) + + return set_host(shm, host_addr, host) end local function set_host_up(shm, host_addr) log.info("Setting host "..host_addr.." as UP") - local host = get_host(shm, host_addr) + local host, err = get_host(shm, host_addr) + if err then + return false, err + end + host.unhealthy_at = 0 - set_host(shm, host_addr, host) + + return set_host(shm, host_addr, host) end local function is_host_up(shm, host_addr) - local host = get_host(shm, host_addr) + local host, err = get_host(shm, host_addr) + if err then + return nil, err + end + return host.unhealthy_at == 0 end local function can_host_be_considered_up(shm, host_addr) - local host = get_host(shm, host_addr) - return is_host_up(shm, host_addr) or (time_utils.get_time() - host.unhealthy_at > host.reconnection_delay) + local host, err = get_host(shm, host_addr) + if err then + return nil, err + end + local is_up, err = is_host_up(shm, host_addr) + if err then + return nil, err + end + + return is_up or (time_utils.get_time() - host.unhealthy_at > host.reconnection_delay) end return { diff --git a/src/cassandra/constants.lua b/src/cassandra/constants.lua new file mode 100644 index 0000000..cf4b517 --- /dev/null +++ b/src/cassandra/constants.lua @@ -0,0 +1,10 @@ +local SUPPORTED_PROTOCOL_VERSIONS = {2, 3} +local DEFAULT_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[#SUPPORTED_PROTOCOL_VERSIONS] + +return { + SUPPORTED_PROTOCOL_VERSIONS = SUPPORTED_PROTOCOL_VERSIONS, + DEFAULT_PROTOCOL_VERSION = DEFAULT_PROTOCOL_VERSION, + MIN_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[1], + MAX_PROTOCOL_VERSION = DEFAULT_PROTOCOL_VERSION, + CQL_VERSION = "3.0.0" +} diff --git a/src/cassandra/consts.lua b/src/cassandra/consts.lua deleted file mode 100644 index d35c7a0..0000000 --- a/src/cassandra/consts.lua +++ /dev/null @@ -1,10 +0,0 @@ -local SUPPORTED_PROTOCOL_VERSION = {2, 3} -local DEFAULT_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSION[#SUPPORTED_PROTOCOL_VERSION] - -return { - SUPPORTED_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSION, - DEFAULT_PROTOCOL_VERSION = DEFAULT_PROTOCOL_VERSION, - MIN_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSION[1], - MAX_PROTOCOL_VERSION = DEFAULT_PROTOCOL_VERSION, - CQL_VERSION = "3.0.0" -} diff --git a/src/cassandra/host_connection.lua b/src/cassandra/host_connection.lua deleted file mode 100644 index 7eb75bf..0000000 --- a/src/cassandra/host_connection.lua +++ /dev/null @@ -1,207 +0,0 @@ ---- Represent one socket to connect to a Cassandra node -local Object = require "cassandra.classic" -local Errors = require "cassandra.errors" -local CONSTS = require "cassandra.consts" -local requests = require "cassandra.requests" -local frame_header = require "cassandra.types.frame_header" -local frame_reader = require "cassandra.frame_reader" -local string_find = string.find - -local FrameReader = frame_reader.FrameReader -local FrameHeader = frame_header.FrameHeader - ---- Constants --- @section constants - -local SOCKET_TYPES = { - NGX = "ngx", - LUASOCKET = "luasocket" -} - ---- Utils --- @section utils - -local function new_socket(self) - local tcp_sock, sock_type - - if ngx and ngx.get_phase ~= nil and ngx.get_phase ~= "init" then - -- lua-nginx-module - tcp_sock = ngx.socket.tcp - sock_type = SOCKET_TYPES.NGX - else - -- fallback to luasocket - tcp_sock = require("socket").tcp - sock_type = SOCKET_TYPES.LUASOCKET - end - - local socket, err = tcp_sock() - if not socket then - error(err) - end - - self.socket = socket - self.socket_type = sock_type -end - ---- HostConnection --- @section host_connection - -local HostConnection = Object:extend() - -function HostConnection:new(host, port, options) - self.host = host - self.port = port - self.address = host..":"..port - self.protocol_version = CONSTS.DEFAULT_PROTOCOL_VERSION - --self.connected = false - - self.log = options.logger - self.socket_options = options.socket_options - - new_socket(self) -end - -function HostConnection:decrease_version() - self.protocol_version = self.protocol_version - 1 -end - ---- Socket operations --- @section socket - -function HostConnection:get_reused_times() - if self.socket_type == SOCKET_TYPES.NGX then - return self.socket:getreusedtimes() - end - - -- luasocket - return 0 -end - -function HostConnection:close() - self.log:info("Closing connection to "..self.address..".") - local res, err = self.socket:close() - if res ~= 1 then - self.log:err("Could not close socket for connection to "..self.address..". "..err) - return false, err - else - --self.connected = false - return true - end -end - -function HostConnection:set_timeout(timeout) - if self.socket_type == SOCKET_TYPES.LUASOCKET then - -- value is in seconds - timeout = timeout / 1000 - end - - self.socket:settimeout(timeout) -end - -local function send_and_receive(self, request) - -- Send frame - local bytes_sent, err = self.socket:send(request:get_full_frame()) - if bytes_sent == nil then - return nil, err - end - - -- Receive frame version byte - local frame_version_byte, err = self.socket:receive(1) - if frame_version_byte == nil then - return nil, err - end - - local n_bytes_to_receive = FrameHeader.size_from_byte(frame_version_byte) - 1 - - -- Receive frame header - local header_bytes, err = self.socket:receive(n_bytes_to_receive) - if header_bytes == nil then - return nil, err - end - - local frameHeader = FrameHeader.from_raw_bytes(frame_version_byte, header_bytes) - - -- Receive frame body - local body_bytes - if frameHeader.body_length > 0 then - body_bytes, err = self.socket:receive(frameHeader.body_length) - if body_bytes == nil then - return nil, err - end - end - - return FrameReader(frameHeader, body_bytes) -end - -function HostConnection:send(request) - request:set_version(self.protocol_version) - - self:set_timeout(self.socket_options.read_timeout) - - local frameReader, err = send_and_receive(self, request) - if err then - if err == "timeout" then - return nil, Errors.TimeoutError(self.address) - else - return nil, Errors.SocketError(self.address, err) - end - end - - --self:close() - - -- result, cql_error - return frameReader:parse() -end - ---- Determine the protocol version to use and send the STARTUP request -local function startup(self) - self.log:info("Startup request. Trying to use protocol v"..self.protocol_version) - - local startup_req = requests.StartupRequest() - return self.send(self, startup_req) -end - -function HostConnection:open() - --if self.connected then return true end - - self:set_timeout(self.socket_options.connect_timeout) - - self.log:info("Connecting to "..self.address) - local ok, err = self.socket:connect(self.host, self.port) - if ok ~= 1 then - self.log:info("Could not connect to "..self.address..". "..err) - return false, err - end - self.log:info("Socket connected to "..self.address) - - -- Startup request if this socket has never been connected to it - if self:get_reused_times() > 0 then - return true - end - - local res, err = startup(self) - if err then - self.log:info("Startup request failed. "..err) - -- Check for incorrect protocol version - if err and err.code == frame_reader.errors.PROTOCOL then - if string_find(err.message, "Invalid or unsupported protocol version:", nil, true) then - self:close() - self:decrease_version() - if self.protocol_version < CONSTS.MIN_PROTOCOL_VERSION then - self.log:err("Connection could not find a supported protocol version.") - else - self.log:info("Decreasing protocol version to v"..self.protocol_version) - return self:open() - end - end - end - - return false, err - elseif res.ready then - --self.connected = true - self.log:info("Host at "..self.address.." is ready with protocol v"..self.protocol_version) - return true - end -end - -return HostConnection diff --git a/src/cassandra/client_options.lua b/src/cassandra/options.lua similarity index 97% rename from src/cassandra/client_options.lua rename to src/cassandra/options.lua index 65fcd65..e263cf4 100644 --- a/src/cassandra/client_options.lua +++ b/src/cassandra/options.lua @@ -39,8 +39,6 @@ local function parse_cluster(options) parse_session(options) - utils.extend_table(DEFAULTS, options) - if type(options.contact_points) ~= "table" then error("contact_points must be a table", 3) end diff --git a/src/cassandra/policies/load_balancing.lua b/src/cassandra/policies/load_balancing.lua index b391439..02e6ebd 100644 --- a/src/cassandra/policies/load_balancing.lua +++ b/src/cassandra/policies/load_balancing.lua @@ -1,4 +1,5 @@ -local storage = require "cassandra.storage" +local cache = require "cassandra.cache" +local log = require "cassandra.log" local math_fmod = math.fmod return { @@ -6,10 +7,10 @@ return { local n = #hosts local counter = 0 - local dict = storage.get_dict(shm) - local plan_index = dict:get("plan_index") - if not plan_index then - dict:set("plan_index", 0) + local dict = cache.get_dict(shm) + local ok, err = dict:add("plan_index", 0) + if not ok then + log.err("Cannot prepare round robin load balancing policy: "..err) end return function(t, i) diff --git a/src/cassandra/requests.lua b/src/cassandra/requests.lua index 87ca3f5..fbe6b47 100644 --- a/src/cassandra/requests.lua +++ b/src/cassandra/requests.lua @@ -1,4 +1,4 @@ -local CONSTS = require "cassandra.consts" +local CONSTS = require "cassandra.constants" local Object = require "cassandra.classic" local Buffer = require "cassandra.buffer" local frame_header = require "cassandra.types.frame_header" @@ -27,7 +27,7 @@ function Request:set_version(version) end function Request:build() - error("mest be implemented") + error("Request:build() must be implemented") end function Request:get_full_frame() diff --git a/src/cassandra/utils/table.lua b/src/cassandra/utils/table.lua index 11bcf5b..a4a88ca 100644 --- a/src/cassandra/utils/table.lua +++ b/src/cassandra/utils/table.lua @@ -1,4 +1,4 @@ -local CONSTS = require "cassandra.consts" +local CONSTS = require "cassandra.constants" local _M = {} From 67a32cc488661a818248433ab1a478fca94a2897 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Thu, 12 Nov 2015 22:07:04 -0800 Subject: [PATCH 21/78] feat(rewrite) timeout options for session + cleanup --- spec/integration/client_spec.lua | 93 ----------------------- spec/unit/cql_types_buffer_spec.lua | 2 +- spec/unit/host_spec.lua | 28 ------- src/cassandra.lua | 86 ++++++++++++--------- src/cassandra/options.lua | 4 +- src/cassandra/policies/load_balancing.lua | 2 +- 6 files changed, 54 insertions(+), 161 deletions(-) delete mode 100644 spec/integration/client_spec.lua delete mode 100644 spec/unit/host_spec.lua diff --git a/spec/integration/client_spec.lua b/spec/integration/client_spec.lua deleted file mode 100644 index 6f06e43..0000000 --- a/spec/integration/client_spec.lua +++ /dev/null @@ -1,93 +0,0 @@ -local Client = require "cassandra.client" -local t_utils = require "cassandra.utils.table" - -local FAKE_CLUSTER = {"0.0.0.1", "0.0.0.2", "0.0.0.3"} ---local contact_points_2_0 = {"127.0.0.1"} -local contact_points_2_1 = {"127.0.0.1"} - -local function client_factory(opts) - t_utils.extend_table({print_log_level = "DEBUG"}, opts) - return Client(opts) -end - -describe("Client", function() - it("should be instanciable", function() - assert.has_no_errors(function() - local client = client_factory({contact_points = FAKE_CLUSTER}) - assert.equal(false, client.connected) - end) - end) - describe("#_connect()", function() - local client - - after_each(function() - local err = client:shutdown() - assert.falsy(err) - end) - - it("should return error if no host is available", function() - client = client_factory({contact_points = FAKE_CLUSTER}) - local err = client:_connect() - assert.truthy(err) - assert.equal("NoHostAvailableError", err.type) - assert.False(client.connected) - end) - it("should connect to a cluster", function() - client = client_factory({contact_points = contact_points_2_1}) - local err = client:_connect() - assert.falsy(err) - assert.True(client.connected) - end) - it("should retrieve cluster information when connecting", function() - client = client_factory({contact_points = contact_points_2_1}) - local err = client:_connect() - assert.falsy(err) - assert.True(client.connected) - - local hosts = client.hosts - assert.truthy(hosts["127.0.0.1"]) - assert.truthy(hosts["127.0.0.2"]) - assert.truthy(hosts["127.0.0.3"]) - - -- Contact point used should have a socket - assert.truthy(hosts[contact_points_2_1[1]].connection.socket) - - for _, host in pairs(hosts) do - assert.truthy(host.address) - assert.truthy(host.cassandra_version) - assert.truthy(host.rack) - assert.truthy(host.datacenter) - assert.truthy(host.connection.port) - assert.truthy(host.connection.protocol_version) - end - end) - it("should downgrade the protocol version if the node does not support the most recent one", function() - pending() - --client = client_factory({contact_points = contact_points_2_0}) - local err = client:_connect() - assert.falsy(err) - assert.True(client.connected) - end) - describe("#execute()", function() - local client = client_factory({contact_points = contact_points_2_1}) - - after_each(function() - local err = client:shutdown() - assert.falsy(err) - end) - - it("should send a request through the configured load balancer", function() - spy.on(client.options.policies.load_balancing, "iterator") - - local res, err = client:execute("SELECT peer FROM system.peers") - - assert.spy(client.options.policies.load_balancing.iterator).was.called() - - assert.falsy(err) - assert.truthy(res) - assert.equal("ROWS", res.type) - assert.equal(2, #res) - end) - end) - end) -end) diff --git a/spec/unit/cql_types_buffer_spec.lua b/spec/unit/cql_types_buffer_spec.lua index 2e8f017..5ebe4c8 100644 --- a/spec/unit/cql_types_buffer_spec.lua +++ b/spec/unit/cql_types_buffer_spec.lua @@ -1,5 +1,5 @@ local Buffer = require "cassandra.buffer" -local CONSTS = require "cassandra.consts" +local CONSTS = require "cassandra.constants" local CQL_TYPES = require "cassandra.types.cql_types" for _, protocol_version in ipairs(CONSTS.SUPPORTED_PROTOCOL_VERSIONS) do diff --git a/spec/unit/host_spec.lua b/spec/unit/host_spec.lua deleted file mode 100644 index 87c2da9..0000000 --- a/spec/unit/host_spec.lua +++ /dev/null @@ -1,28 +0,0 @@ -local Host = require "cassandra.host" - -local opts = { - logger = { - warn = function()end, - info = function()end - } -} - -describe("Host", function() - local host - it("should be instanciable", function() - host = Host("127.0.0.1:9042", opts) - host.reconnection_delay = 0 - assert.equal(0, host.unhealthy_at) - assert.True(host:can_be_considered_up()) - end) - it("should be possible to mark it as DOWN", function() - host:set_down() - assert.equal(os.time() * 1000, host.unhealthy_at) - assert.False(host:can_be_considered_up()) - end) - it("should be possible to mark as UP", function() - host:set_up() - assert.equal(0, host.unhealthy_at) - assert.True(host:can_be_considered_up()) - end) -end) diff --git a/src/cassandra.lua b/src/cassandra.lua index a3c23d2..2956735 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -50,7 +50,7 @@ function Host:new(address, options) self.host = host self.port = port - self.address = host..":"..port + self.address = address self.protocol_version = CONSTS.DEFAULT_PROTOCOL_VERSION self.options = options @@ -58,6 +58,10 @@ function Host:new(address, options) new_socket(self) end +function Host:decrease_version() + self.protocol_version = self.protocol_version - 1 +end + local function send_and_receive(self, request) -- Send frame local bytes_sent, err = self.socket:send(request:get_full_frame()) @@ -96,7 +100,7 @@ end function Host:send(request) request:set_version(self.protocol_version) - --self:set_timeout(self.socket_options.read_timeout) + self:set_timeout(self.options.socket_options.read_timeout) local frameReader, err = send_and_receive(self, request) if err then @@ -121,6 +125,8 @@ end function Host:connect() log.info("Connecting to "..self.address) + self:set_timeout(self.options.socket_options.connect_timeout) + local ok, err = self.socket:connect(self.host, self.port) if ok ~= 1 then log.info("Could not connect to "..self.address..". Reason: "..err) @@ -130,6 +136,7 @@ function Host:connect() log.info("Session connected to "..self.address) if self:get_reused_times() > 0 then + -- No need for startup request return true end @@ -158,6 +165,15 @@ function Host:connect() end end +function Host:set_timeout(t) + if self.socket_type == "luasocket" then + -- value is in seconds + t = t / 1000 + end + + return self.socket:settimeout(t) +end + function Host:get_reused_times() if self.socket_type == "ngx" then local count, err = self.socket:getreusedtimes() @@ -194,12 +210,25 @@ function Host:close() end end ---- Request handler --- @section request_handler +--- Session +-- A short-lived session, cluster-aware through the cache. +-- Uses a load balancing policy to select a coordinator on which to perform requests. +-- @section session + +local Session = {} -local RequestHandler = Object:extend() +function Session:new(options) + options = opts.parse_session(options) -function RequestHandler.get_first_host(hosts) + local s = { + options = options, + coordinator = nil -- to be determined by load balancing policy + } + + return setmetatable(s, {__index = self}) +end + +function Session.get_first_coordinator(hosts) local errors = {} for _, host in ipairs(hosts) do local connected, err = host:connect() @@ -213,24 +242,7 @@ function RequestHandler.get_first_host(hosts) return nil, Errors.NoHostAvailableError(errors) end ---- Session --- An expandable session, cluster-aware through the cache. --- Uses a load balancing policy to select nodes on which to perform requests. --- @section session - -local Session = {} - -function Session:new(options) - options = opts.parse_session(options) - - local s = { - options = options - } - - return setmetatable(s, {__index = self}) -end - -function Session:get_next_connection() +function Session:get_next_coordinator() local errors = {} local iter = self.options.policies.load_balancing @@ -243,11 +255,11 @@ function Session:get_next_connection() local can_host_be_considered_up, err = cache.can_host_be_considered_up(self.options.shm, addr) if err then return nil, err - end - if can_host_be_considered_up then + elseif can_host_be_considered_up then local host = Host(addr, self.options) local connected, err = host:connect() if connected then + self.coordinator = host return host else errors[addr] = err @@ -261,29 +273,29 @@ function Session:get_next_connection() end function Session:execute(query) - local host, err = self:get_next_connection() + local coordinator, err = self:get_next_coordinator() if err then return nil, err end - log.info("Acquired connection through load balancing policy: "..host.address) + log.info("Acquired connection through load balancing policy: "..coordinator.address) local query_request = Requests.QueryRequest(query) - local result, err = host:send(query_request) + local result, err = coordinator:send(query_request) if err then return nil, err end -- Success! Make sure to re-up node in case it was marked as DOWN - local ok, err = cache.set_host_up(self.options.shm, host.host) + local ok, err = cache.set_host_up(self.options.shm, coordinator.host) if err then return nil, err end - if host.socket_type == "ngx" then - host:set_keep_alive() + if coordinator.socket_type == "ngx" then + coordinator:set_keep_alive() else - host:close() + coordinator:close() end return result @@ -330,7 +342,7 @@ local SELECT_LOCAL_QUERY = "SELECT data_center,rack,rpc_address,release_version function Cassandra.refresh_hosts(contact_points_hosts, options) log.info("Refreshing local and peers info") - local host, err = RequestHandler.get_first_host(contact_points_hosts) + local coordinator, err = Session.get_first_coordinator(contact_points_hosts) if err then return false, err end @@ -339,7 +351,7 @@ function Cassandra.refresh_hosts(contact_points_hosts, options) local peers_query = Requests.QueryRequest(SELECT_PEERS_QUERY) local hosts = {} - local rows, err = host:send(local_query) + local rows, err = coordinator:send(local_query) if err then return false, err end @@ -356,7 +368,7 @@ function Cassandra.refresh_hosts(contact_points_hosts, options) hosts[address] = local_host log.info("Local info retrieved") - rows, err = host:send(peers_query) + rows, err = coordinator:send(peers_query) if err then return false, err end @@ -375,6 +387,8 @@ function Cassandra.refresh_hosts(contact_points_hosts, options) end log.info("Peers info retrieved") + coordinator:close() + -- Store cluster mapping for future sessions local addresses = {} for addr, host in pairs(hosts) do diff --git a/src/cassandra/options.lua b/src/cassandra/options.lua index e263cf4..d51f7f5 100644 --- a/src/cassandra/options.lua +++ b/src/cassandra/options.lua @@ -14,8 +14,8 @@ local DEFAULTS = { default_port = 9042 }, socket_options = { - connect_timeout = 5000, - read_timeout = 12000 + connect_timeout = 1000, + read_timeout = 2000 } } diff --git a/src/cassandra/policies/load_balancing.lua b/src/cassandra/policies/load_balancing.lua index 02e6ebd..b36e4be 100644 --- a/src/cassandra/policies/load_balancing.lua +++ b/src/cassandra/policies/load_balancing.lua @@ -9,7 +9,7 @@ return { local dict = cache.get_dict(shm) local ok, err = dict:add("plan_index", 0) - if not ok then + if not ok and err ~= "exists" then log.err("Cannot prepare round robin load balancing policy: "..err) end From 4d851710ded99ba694695b08255f473661618002 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Thu, 12 Nov 2015 22:08:33 -0800 Subject: [PATCH 22/78] tests: add integration tests with Test::Nginx --- .gitignore | 1 + t/01-cassandra.t | 98 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 t/01-cassandra.t diff --git a/.gitignore b/.gitignore index de069ae..e1c601f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ nginx_tmp +t/servroot diff --git a/t/01-cassandra.t b/t/01-cassandra.t new file mode 100644 index 0000000..3b2b897 --- /dev/null +++ b/t/01-cassandra.t @@ -0,0 +1,98 @@ +use Test::Nginx::Socket::Lua; +use Cwd qw(cwd); + +repeat_each(2); + +plan tests => repeat_each() * blocks() * 3; + +my $pwd = cwd(); + +our $HttpConfig = <<_EOC_; + lua_package_path "$pwd/src/?.lua;;"; +_EOC_ + +our $SpawnCluster = <<_EOC_; + lua_shared_dict cassandra 1m; + init_by_lua ' + local cassandra = require "cassandra" + local ok, err = cassandra.spawn_cluster({ + shm = "cassandra", + contact_points = {"127.0.0.1", "127.0.0.2"} + }) + if not ok then + ngx.log(ngx.ERR, tostring(err)) + end + '; +_EOC_ + +run_tests(); + +__DATA__ + +=== TEST 1: spawn cluster +--- http_config eval +"$::HttpConfig + $::SpawnCluster" +--- config + location /t { + return 200; + } +--- request +GET /t +--- response_body + +--- no_error_log +[error] + + + +=== TEST 2: spawn session +--- http_config eval +"$::HttpConfig + $::SpawnCluster" +--- config + location /t { + content_by_lua ' + local cassandra = require "cassandra" + local session = cassandra.spawn_session {shm = "cassandra"} + '; + } +--- request +GET /t +--- response_body + +--- no_error_log +[error] + + + +=== TEST 3: session:execute() +--- http_config eval +"$::HttpConfig + $::SpawnCluster" +--- config + location /t { + content_by_lua ' + local cassandra = require "cassandra" + local session = cassandra.spawn_session {shm = "cassandra"} + local rows, err = session:execute("SELECT key FROM system.local") + if err then + ngx.log(ngx.ERR, tostring(err)) + ngx.exit(500) + else + ngx.say("type: "..rows.type) + ngx.say("#rows: "..#rows) + for _, row in ipairs(rows) do + ngx.say(row["key"]) + end + end + '; + } +--- request +GET /t +--- response_body +type: ROWS +#rows: 1 +local +--- no_error_log +[error] From 5fdec2f7d18ff26b217f6701b623e309f770bc34 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Sat, 14 Nov 2015 17:33:47 -0800 Subject: [PATCH 23/78] feat(rewrite) integration tests for non ngx_lua --- spec/integration/cassandra_spec.lua | 61 +++++++++++++++++++++++++++++ src/cassandra/cache.lua | 2 +- src/cassandra/log.lua | 40 +++++++++++++++---- src/cassandra/options.lua | 6 ++- 4 files changed, 100 insertions(+), 9 deletions(-) create mode 100644 spec/integration/cassandra_spec.lua diff --git a/spec/integration/cassandra_spec.lua b/spec/integration/cassandra_spec.lua new file mode 100644 index 0000000..f8a5248 --- /dev/null +++ b/spec/integration/cassandra_spec.lua @@ -0,0 +1,61 @@ +--- Pure Lua integration tests. +-- lua-cassandra is built with support for pure Lua, outside of ngx_lua, +-- with fallback on LuaSocket when it is the case. Those integration tests must +-- mimic the ones running in ngx_lua. + +local cassandra = require "cassandra" +local log = require "cassandra.log" + +-- Define log level for tests +log.set_lvl("ERR") + +local _shm = "cassandra" +local _contact_points = {"127.0.0.1", "127.0.0.2"} + +describe("spawn cluster", function() + it("should require a 'shm' option", function() + assert.has_error(function() + cassandra.spawn_cluster({ + shm = nil, + contact_points = _contact_points + }) + end, "shm is required for spawning a cluster/session") + end) + it("should spawn a cluster", function() + local ok, err = cassandra.spawn_cluster({ + shm = _shm, + contact_points = _contact_points + }) + assert.falsy(err) + assert.True(ok) + end) + it("should retrieve cluster infos in spawned cluster's shm", function() + local cache = require "cassandra.cache" + local dict = cache.get_dict(_shm) + local hosts = cache.get_hosts(_shm) + -- index of hosts + assert.equal(3, #hosts) + -- hosts details + for _, host_addr in ipairs(hosts) do + local host_details = cache.get_host(_shm, host_addr) + assert.truthy(host_details) + end + end) +end) + +describe("spawn session", function() + it("should require a 'shm' option", function() + assert.has_error(function() + cassandra.spawn_session({ + shm = nil + }) + end, "shm is required for spawning a cluster/session") + end) + it("should spawn a session", function() + local session, err = cassandra.spawn_session({ + shm = _shm + }) + assert.falsy(err) + assert.truthy(session) + end) +end) diff --git a/src/cassandra/cache.lua b/src/cassandra/cache.lua index e487183..cbf4c84 100644 --- a/src/cassandra/cache.lua +++ b/src/cassandra/cache.lua @@ -16,7 +16,7 @@ function SharedDict:new() end function SharedDict:get(key) - return self.data[key], 0 + return self.data[key], nil end function SharedDict:set(key, value) diff --git a/src/cassandra/log.lua b/src/cassandra/log.lua index 79ae6a5..c14121d 100644 --- a/src/cassandra/log.lua +++ b/src/cassandra/log.lua @@ -1,13 +1,39 @@ -local ngx_log = ngx.log +--- Logging wrapper +-- lua-cassandra is built with support for pure Lua, outside of ngx_lua, +-- this module provides a fallback to `print` when lua-cassandra runs +-- outside of ngx_lua. + +local is_ngx = ngx ~= nil +local ngx_log = is_ngx and ngx.log +local string_format = string.format + +-- ngx_lua levels redefinition for helpers and +-- when outside of ngx_lua. +local LEVELS = { + ERR = 1, + WARN = 2, + INFO = 3, + DEBUG = 4 +} + +-- Default logging level when outside of ngx_lua. +local cur_lvl = LEVELS.INFO local log = {} -for _, lvl in ipairs({"ERR", "WARN", "INFO", "DEBUG"}) do - log[lvl:lower()] = function(...) - if ngx ~= nil and ngx.get_phase() ~= "init" then - ngx_log(ngx[lvl], ...) - else - print(...) +function log.set_lvl(lvl_name) + if is_ngx then return end + if LEVELS[lvl_name] ~= nil then + cur_lvl = LEVELS[lvl_name] + end +end + +for lvl_name, lvl in pairs(LEVELS) do + log[lvl_name:lower()] = function(...) + if is_ngx and ngx.get_phase() ~= "init" then + ngx_log(ngx[lvl_name], ...) + elseif lvl <= cur_lvl then + print(string_format("%s -- %s", lvl_name, ...)) end end end diff --git a/src/cassandra/options.lua b/src/cassandra/options.lua index d51f7f5..6c4145d 100644 --- a/src/cassandra/options.lua +++ b/src/cassandra/options.lua @@ -4,7 +4,7 @@ local utils = require "cassandra.utils.table" -- @section defaults local DEFAULTS = { - shm = "cassandra", + shm = nil, -- required contact_points = {}, policies = { address_resolution = require "cassandra.policies.address_resolution", @@ -28,6 +28,10 @@ local function parse_session(options) --error("keyspace must be a string") --end + assert(options.shm ~= nil, "shm is required for spawning a cluster/session") + assert(type(options.shm) == "string", "shm must be a string") + assert(options.shm ~= "", "shm must be a valid string") + assert(type(options.protocol_options.default_port) == "number", "protocol default_port must be a number") assert(type(options.policies.address_resolution) == "function", "address_resolution policy must be a function") From b29691f9c07cf09bcfdbca92294b9a9913018428 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Mon, 16 Nov 2015 14:40:46 -0800 Subject: [PATCH 24/78] feat(rewrite) retry policy and error handling --- spec/load.lua | 29 ++++-- src/cassandra.lua | 156 +++++++++++++++++++++---------- src/cassandra/cache.lua | 2 +- src/cassandra/options.lua | 3 +- src/cassandra/policies/retry.lua | 31 ++++++ 5 files changed, 162 insertions(+), 59 deletions(-) create mode 100644 src/cassandra/policies/retry.lua diff --git a/spec/load.lua b/spec/load.lua index 3c3c634..0b200a5 100644 --- a/spec/load.lua +++ b/spec/load.lua @@ -1,14 +1,29 @@ -package.path = package.path..";src/?.lua" -local Client = require "cassandra.client" +package.path = "src/?.lua;"..package.path +local inspect = require "inspect" +local cassandra = require "cassandra" +local log = require "cassandra.log" -local client = Client({contact_points = {"127.0.0.1", "127.0.0.2"}, print_log_level = "INFO"}) +log.set_lvl("ERR") -for i = 1, 10000 do - local res, err = client:execute("SELECT peer FROM system.peers") +local ok, err = cassandra.spawn_cluster { + shm = "cassandra", + contact_points = {"127.0.0.1", "127.0.0.2"} +} +assert(err == nil, inspect(err)) + +local session, err = cassandra.spawn_session { + shm = "cassandra" +} +assert(err == nil, inspect(err)) + +local i = 0 +while true do + i = i + 1 + local res, err = session:execute("SELECT peer FROM system.peers") if err then - error(err) + print(inspect(err)) + error() end print("Request "..i.." successful.") end -client:shutdown() diff --git a/src/cassandra.lua b/src/cassandra.lua index 2956735..fd5fe93 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -1,13 +1,13 @@ +local log = require "cassandra.log" +local opts = require "cassandra.options" +local cache = require "cassandra.cache" local Object = require "cassandra.classic" local CONSTS = require "cassandra.constants" local Errors = require "cassandra.errors" local Requests = require "cassandra.requests" -local cache = require "cassandra.cache" +local string_utils = require "cassandra.utils.string" local frame_header = require "cassandra.types.frame_header" local frame_reader = require "cassandra.frame_reader" -local opts = require "cassandra.options" -local string_utils = require "cassandra.utils.string" -local log = require "cassandra.log" local table_insert = table.insert local string_find = string.find @@ -210,25 +210,22 @@ function Host:close() end end ---- Session --- A short-lived session, cluster-aware through the cache. --- Uses a load balancing policy to select a coordinator on which to perform requests. --- @section session +--- Request Handler +-- @section request_handler -local Session = {} +local RequestHandler = {} -function Session:new(options) - options = opts.parse_session(options) - - local s = { +function RequestHandler:new(request, options) + local o = { + request = request, options = options, - coordinator = nil -- to be determined by load balancing policy + n_retries = 0 } - return setmetatable(s, {__index = self}) + return setmetatable(o, {__index = self}) end -function Session.get_first_coordinator(hosts) +function RequestHandler.get_first_coordinator(hosts) local errors = {} for _, host in ipairs(hosts) do local connected, err = host:connect() @@ -242,19 +239,19 @@ function Session.get_first_coordinator(hosts) return nil, Errors.NoHostAvailableError(errors) end -function Session:get_next_coordinator() +function RequestHandler:get_next_coordinator() local errors = {} local iter = self.options.policies.load_balancing - local hosts, err = cache.get_hosts(self.options.shm) + local hosts, cache_err = cache.get_hosts(self.options.shm) if err then - return nil, err + return nil, cache_err end for _, addr in iter(self.options.shm, hosts) do - local can_host_be_considered_up, err = cache.can_host_be_considered_up(self.options.shm, addr) - if err then - return nil, err + local can_host_be_considered_up, cache_err = cache.can_host_be_considered_up(self.options.shm, addr) + if cache_err then + return nil, cache_err elseif can_host_be_considered_up then local host = Host(addr, self.options) local connected, err = host:connect() @@ -262,6 +259,11 @@ function Session:get_next_coordinator() self.coordinator = host return host else + -- bad host, setting DOWN + local ok, cache_err = cache.set_host_down(self.options.shm, addr) + if not ok then + return nil, cache_err + end errors[addr] = err end else @@ -272,7 +274,7 @@ function Session:get_next_coordinator() return nil, Errors.NoHostAvailableError(errors) end -function Session:execute(query) +function RequestHandler:send() local coordinator, err = self:get_next_coordinator() if err then return nil, err @@ -280,17 +282,7 @@ function Session:execute(query) log.info("Acquired connection through load balancing policy: "..coordinator.address) - local query_request = Requests.QueryRequest(query) - local result, err = coordinator:send(query_request) - if err then - return nil, err - end - - -- Success! Make sure to re-up node in case it was marked as DOWN - local ok, err = cache.set_host_up(self.options.shm, coordinator.host) - if err then - return nil, err - end + local result, err = coordinator:send(self.request) if coordinator.socket_type == "ngx" then coordinator:set_keep_alive() @@ -298,32 +290,96 @@ function Session:execute(query) coordinator:close() end + if err then + return self:handle_error(err) + end + + -- Success! Make sure to re-up node in case it was marked as DOWN + local ok, cache_err = cache.set_host_up(self.options.shm, coordinator.host) + if err then + return nil, cache_err + end + return result end -function Session:handle_error(err) - if err.type == "SocketError" then +function RequestHandler:handle_error(err) + local retry_policy = self.options.policies.retry + local decision = retry_policy.decisions.throw + + if err.type == "SocketError" or err.type == "TimeoutError" then -- host seems unhealthy - self.host:set_down() - -- always retry - elseif err.type == "TimeoutError" then - -- on timeout + local ok, cache_err = cache.set_host_down(self.options.shm, self.coordinator.address) + if not ok then + return nil, cache_err + end + -- always retry, another node will be picked + return self:retry() elseif err.type == "ResponseError" then + local request_infos = { + handler = self, + request = self.request, + n_retries = self.n_retries + } if err.code == CQL_Errors.OVERLOADED or err.code == CQL_Errors.IS_BOOTSTRAPPING or err.code == CQL_Errors.TRUNCATE_ERROR then - -- always retry + -- always retry, we will hit another node + return self:retry() elseif err.code == CQL_Errors.UNAVAILABLE_EXCEPTION then - -- make retry decision based on retry_policy on_unavailable + decision = retry_policy.on_unavailable(request_infos) elseif err.code == CQL_Errors.READ_TIMEOUT then - -- make retry decision based on retry_policy read_timeout + decision = retry_policy.on_read_timeout(request_infos) elseif err.code == CQL_Errors.WRITE_TIMEOUT then - -- make retry decision based on retry_policy write_timeout + decision = retry_policy.on_write_timeout(request_infos) + elseif err.code == CQL_Errors.UNPREPARED then + -- re prepare and retry end end - -- this error needs to be reported to the client + if decision == retry_policy.decisions.retry then + return self:retry() + end + + -- this error needs to be reported to the session return nil, err end +function RequestHandler:retry() + self.n_retries = self.n_retries + 1 + log.info("Retrying request") + return self:send() +end + +--- Session +-- A short-lived session, cluster-aware through the cache. +-- Uses a load balancing policy to select a coordinator on which to perform requests. +-- @section session + +local Session = {} + +function Session:new(options) + options = opts.parse_session(options) + + local s = { + options = options, + coordinator = nil -- to be determined by load balancing policy + } + + return setmetatable(s, {__index = self}) +end + + +function Session:execute(query) + local query_request = Requests.QueryRequest(query) + local request_handler = RequestHandler:new(query_request, self.options) + return request_handler:send() +end + +function Session:close() + if self.coordinator ~= nil then + return self.coordinator:close() + end +end + --- Cassandra -- @section cassandra @@ -342,7 +398,7 @@ local SELECT_LOCAL_QUERY = "SELECT data_center,rack,rpc_address,release_version function Cassandra.refresh_hosts(contact_points_hosts, options) log.info("Refreshing local and peers info") - local coordinator, err = Session.get_first_coordinator(contact_points_hosts) + local coordinator, err = RequestHandler.get_first_coordinator(contact_points_hosts) if err then return false, err end @@ -363,7 +419,7 @@ function Cassandra.refresh_hosts(contact_points_hosts, options) cassandra_version = row["release_version"], protocol_versiom = row["native_protocol_version"], unhealthy_at = 0, - reconnection_delay = 5 + reconnection_delay = 5000 } hosts[address] = local_host log.info("Local info retrieved") @@ -382,7 +438,7 @@ function Cassandra.refresh_hosts(contact_points_hosts, options) cassandra_version = row["release_version"], protocol_version = local_host.native_protocol_version, unhealthy_at = 0, - reconnection_delay = 5 + reconnection_delay = 5000 } end log.info("Peers info retrieved") @@ -393,9 +449,9 @@ function Cassandra.refresh_hosts(contact_points_hosts, options) local addresses = {} for addr, host in pairs(hosts) do table_insert(addresses, addr) - local ok, err = cache.set_host(options.shm, addr, host) + local ok, cache_err = cache.set_host(options.shm, addr, host) if err then - return false, err + return false, cache_err end end diff --git a/src/cassandra/cache.lua b/src/cassandra/cache.lua index cbf4c84..92cb49a 100644 --- a/src/cassandra/cache.lua +++ b/src/cassandra/cache.lua @@ -166,7 +166,7 @@ local function can_host_be_considered_up(shm, host_addr) return nil, err end - return is_up or (time_utils.get_time() - host.unhealthy_at > host.reconnection_delay) + return is_up or (time_utils.get_time() - host.unhealthy_at >= host.reconnection_delay) end return { diff --git a/src/cassandra/options.lua b/src/cassandra/options.lua index 6c4145d..ba902d6 100644 --- a/src/cassandra/options.lua +++ b/src/cassandra/options.lua @@ -8,7 +8,8 @@ local DEFAULTS = { contact_points = {}, policies = { address_resolution = require "cassandra.policies.address_resolution", - load_balancing = require("cassandra.policies.load_balancing").RoundRobin + load_balancing = require("cassandra.policies.load_balancing").RoundRobin, + retry = require("cassandra.policies.retry") }, protocol_options = { default_port = 9042 diff --git a/src/cassandra/policies/retry.lua b/src/cassandra/policies/retry.lua new file mode 100644 index 0000000..a039d81 --- /dev/null +++ b/src/cassandra/policies/retry.lua @@ -0,0 +1,31 @@ +local DECISIONS = { + throw = 0, + retry = 1 +} + +local function on_unavailable(request_infos) + return DECISIONS.retry +end + +local function on_read_timeout(request_infos) + if request_infos.n_retries > 0 then + return DECISIONS.throw + end + + return DECISIONS.retry +end + +local function on_write_timeout(request_infos) + if request_infos.n_retries > 0 then + return DECISIONS.throw + end + + return DECISIONS.retry +end + +return { + on_unavailable = on_unavailable, + on_read_timeout = on_read_timeout, + on_write_timeout = on_write_timeout, + decisions = DECISIONS +} From 95a7894a3cc0c853cec2e4a311c1e703aac726a5 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Mon, 16 Nov 2015 17:42:05 -0800 Subject: [PATCH 25/78] feat(rewrite) reconnection policies - constant and exponential policies --- spec/unit/reconnection_policy_spec.lua | 58 ++++++++++++++++++ src/cassandra.lua | 73 +++++++++++++++++++---- src/cassandra/cache.lua | 51 ---------------- src/cassandra/options.lua | 5 +- src/cassandra/policies/load_balancing.lua | 12 ++-- src/cassandra/policies/reconnection.lua | 51 ++++++++++++++++ 6 files changed, 181 insertions(+), 69 deletions(-) create mode 100644 spec/unit/reconnection_policy_spec.lua create mode 100644 src/cassandra/policies/reconnection.lua diff --git a/spec/unit/reconnection_policy_spec.lua b/spec/unit/reconnection_policy_spec.lua new file mode 100644 index 0000000..4eab019 --- /dev/null +++ b/spec/unit/reconnection_policy_spec.lua @@ -0,0 +1,58 @@ +local reconnection_policies = require "cassandra.policies.reconnection" + +local host_options = { + shm = "cassandra", + protocol_options = { + default_port = 9042 + } +} + +describe("Reconnection policies", function() + describe("constant", function() + local delay = 5000 + local policy = reconnection_policies.Constant(delay) + + it("should return a constant delay", function() + assert.has_no_error(function() + policy.new_schedule() + end) + + for i = 1, 10 do + assert.equal(delay, policy.next()) + end + end) + end) + describe("exponential", function() + local base_delay, max_delay = 1000, 10 * 60 * 1000 + local policy = reconnection_policies.SharedExponential(base_delay, max_delay) + local host1 = {address = "127.0.0.1", options = host_options} + local host2 = {address = "127.0.0.2", options = host_options} + + policy.new_schedule(host1) + policy.new_schedule(host2) + + it("should return an exponential delay", function() + assert.equal(1000, policy.next(host1)) + assert.equal(4000, policy.next(host1)) + assert.equal(9000, policy.next(host1)) + assert.equal(16000, policy.next(host1)) + assert.equal(25000, policy.next(host1)) + for i = 1, 19 do + policy.next(host1) + end + assert.equal(600000, policy.next(host1)) + assert.equal(600000, policy.next(host1)) + end) + it("should allow for different schedules", function() + assert.equal(1000, policy.next(host2)) + assert.equal(4000, policy.next(host2)) + assert.equal(9000, policy.next(host2)) + assert.equal(16000, policy.next(host2)) + end) + it("should be possible to schedule a new policy, aka 'reset' it", function() + policy.new_schedule(host1) + assert.equal(1000, policy.next(host1)) + assert.equal(4000, policy.next(host1)) + end) + end) +end) diff --git a/src/cassandra.lua b/src/cassandra.lua index fd5fe93..b86e5ee 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -5,6 +5,7 @@ local Object = require "cassandra.classic" local CONSTS = require "cassandra.constants" local Errors = require "cassandra.errors" local Requests = require "cassandra.requests" +local time_utils = require "cassandra.utils.time" local string_utils = require "cassandra.utils.string" local frame_header = require "cassandra.types.frame_header" local frame_reader = require "cassandra.frame_reader" @@ -54,6 +55,7 @@ function Host:new(address, options) self.protocol_version = CONSTS.DEFAULT_PROTOCOL_VERSION self.options = options + self.reconnection_policy = self.options.policies.reconnection new_socket(self) end @@ -210,6 +212,59 @@ function Host:close() end end +function Host:set_down() + log.info("Setting host "..self.address.." as DOWN") + local host_infos, err = cache.get_host(self.options.shm, self.address) + if err then + return false, err + end + + host_infos.unhealthy_at = time_utils.get_time() + host_infos.reconnection_delay = self.reconnection_policy.next(self) + + return cache.set_host(self.options.shm, self.address, host_infos) +end + +function Host:set_up() + local host_infos, err = cache.get_host(self.options.shm, self.address) + if err then + return false, err + end + + -- host was previously marked a DOWN + if host_infos.unhealthy_at ~= 0 then + log.info("Setting host "..self.address.." as UP") + host_infos.unhealthy_at = 0 + -- reset schedule for reconnection delay + self.reconnection_policy.new_schedule(self) + return cache.set_host(self.options.shm, self.address, host_infos) + end + + return true +end + +function Host:is_up() + local host_infos, err = cache.get_host(self.options.shm, self.address) + if err then + return nil, err + end + + return host_infos.unhealthy_at == 0 +end + +function Host:can_be_considered_up() + local host_infos, err = cache.get_host(self.options.shm, self.address) + if err then + return nil, err + end + local is_up, err = self:is_up() + if err then + return nil, err + end + + return is_up or (time_utils.get_time() - host_infos.unhealthy_at >= host_infos.reconnection_delay) +end + --- Request Handler -- @section request_handler @@ -249,18 +304,18 @@ function RequestHandler:get_next_coordinator() end for _, addr in iter(self.options.shm, hosts) do - local can_host_be_considered_up, cache_err = cache.can_host_be_considered_up(self.options.shm, addr) + local host = Host(addr, self.options) + local can_host_be_considered_up, cache_err = host:can_be_considered_up() if cache_err then return nil, cache_err elseif can_host_be_considered_up then - local host = Host(addr, self.options) local connected, err = host:connect() if connected then self.coordinator = host return host else -- bad host, setting DOWN - local ok, cache_err = cache.set_host_down(self.options.shm, addr) + local ok, cache_err = host:set_down() if not ok then return nil, cache_err end @@ -295,7 +350,7 @@ function RequestHandler:send() end -- Success! Make sure to re-up node in case it was marked as DOWN - local ok, cache_err = cache.set_host_up(self.options.shm, coordinator.host) + local ok, cache_err = coordinator:set_up() if err then return nil, cache_err end @@ -309,7 +364,7 @@ function RequestHandler:handle_error(err) if err.type == "SocketError" or err.type == "TimeoutError" then -- host seems unhealthy - local ok, cache_err = cache.set_host_down(self.options.shm, self.coordinator.address) + local ok, cache_err = self.coordinator:set_down() if not ok then return nil, cache_err end @@ -331,7 +386,7 @@ function RequestHandler:handle_error(err) elseif err.code == CQL_Errors.WRITE_TIMEOUT then decision = retry_policy.on_write_timeout(request_infos) elseif err.code == CQL_Errors.UNPREPARED then - -- re prepare and retry + -- re-prepare and retry end end @@ -418,8 +473,7 @@ function Cassandra.refresh_hosts(contact_points_hosts, options) rack = row["rack"], cassandra_version = row["release_version"], protocol_versiom = row["native_protocol_version"], - unhealthy_at = 0, - reconnection_delay = 5000 + unhealthy_at = 0 } hosts[address] = local_host log.info("Local info retrieved") @@ -437,8 +491,7 @@ function Cassandra.refresh_hosts(contact_points_hosts, options) rack = row["rack"], cassandra_version = row["release_version"], protocol_version = local_host.native_protocol_version, - unhealthy_at = 0, - reconnection_delay = 5000 + unhealthy_at = 0 } end log.info("Peers info retrieved") diff --git a/src/cassandra/cache.lua b/src/cassandra/cache.lua index 92cb49a..13bee2b 100644 --- a/src/cassandra/cache.lua +++ b/src/cassandra/cache.lua @@ -1,7 +1,6 @@ local json = require "cjson" local log = require "cassandra.log" local string_utils = require "cassandra.utils.string" -local time_utils = require "cassandra.utils.time" local table_concat = table.concat local in_ngx = ngx ~= nil local shared @@ -123,60 +122,10 @@ local function get_host(shm, host_addr) return json.decode(value) end -local function set_host_down(shm, host_addr) - log.warn("Setting host "..host_addr.." as DOWN") - local host, err = get_host(shm, host_addr) - if err then - return false, err - end - - host.unhealthy_at = time_utils.get_time() - - return set_host(shm, host_addr, host) -end - -local function set_host_up(shm, host_addr) - log.info("Setting host "..host_addr.." as UP") - local host, err = get_host(shm, host_addr) - if err then - return false, err - end - - host.unhealthy_at = 0 - - return set_host(shm, host_addr, host) -end - -local function is_host_up(shm, host_addr) - local host, err = get_host(shm, host_addr) - if err then - return nil, err - end - - return host.unhealthy_at == 0 -end - -local function can_host_be_considered_up(shm, host_addr) - local host, err = get_host(shm, host_addr) - if err then - return nil, err - end - local is_up, err = is_host_up(shm, host_addr) - if err then - return nil, err - end - - return is_up or (time_utils.get_time() - host.unhealthy_at >= host.reconnection_delay) -end - return { get_dict = get_dict, get_host = get_host, set_host = set_host, set_hosts = set_hosts, get_hosts = get_hosts, - set_host_up = set_host_up, - set_host_down = set_host_down, - is_host_up = is_host_up, - can_host_be_considered_up = can_host_be_considered_up } diff --git a/src/cassandra/options.lua b/src/cassandra/options.lua index ba902d6..8ea58cc 100644 --- a/src/cassandra/options.lua +++ b/src/cassandra/options.lua @@ -8,8 +8,9 @@ local DEFAULTS = { contact_points = {}, policies = { address_resolution = require "cassandra.policies.address_resolution", - load_balancing = require("cassandra.policies.load_balancing").RoundRobin, - retry = require("cassandra.policies.retry") + load_balancing = require("cassandra.policies.load_balancing").SharedRoundRobin, + retry = require("cassandra.policies.retry"), + reconnection = require("cassandra.policies.reconnection").SharedExponential(1000, 10 * 60 * 1000) }, protocol_options = { default_port = 9042 diff --git a/src/cassandra/policies/load_balancing.lua b/src/cassandra/policies/load_balancing.lua index b36e4be..85092f7 100644 --- a/src/cassandra/policies/load_balancing.lua +++ b/src/cassandra/policies/load_balancing.lua @@ -1,22 +1,22 @@ -local cache = require "cassandra.cache" local log = require "cassandra.log" +local cache = require "cassandra.cache" local math_fmod = math.fmod return { - RoundRobin = function(shm, hosts) + SharedRoundRobin = function(shm, hosts) local n = #hosts local counter = 0 local dict = cache.get_dict(shm) - local ok, err = dict:add("plan_index", 0) + local ok, err = dict:add("rr_plan_index", 0) if not ok and err ~= "exists" then - log.err("Cannot prepare round robin load balancing policy: "..err) + log.err("Cannot prepare shared round robin load balancing policy: "..err) end return function(t, i) - local plan_index = dict:get("plan_index") + local plan_index = dict:get("rr_plan_index") local mod = math_fmod(plan_index, n) + 1 - dict:incr("plan_index", 1) + dict:incr("rr_plan_index", 1) counter = counter + 1 if counter <= n then diff --git a/src/cassandra/policies/reconnection.lua b/src/cassandra/policies/reconnection.lua new file mode 100644 index 0000000..d5a5161 --- /dev/null +++ b/src/cassandra/policies/reconnection.lua @@ -0,0 +1,51 @@ +local cache = require "cassandra.cache" +local math_pow = math.pow +local math_min = math.min + +local function constant_reconnection_policy(delay) + return { + new_schedule = function() end, + next = function() + return delay + end + } +end + +local function shared_exponential_reconnection_policy(base_delay, max_delay) + return { + new_schedule = function(host) + local index_key = "exp_reconnection_idx_"..host.address + local dict = cache.get_dict(host.options.shm) + + local ok, err = dict:set(index_key, 0) + if not ok then + log.err("Cannot reset schedule for shared exponential reconnection policy: "..err) + end + end, + next = function(host) + local index_key = "exp_reconnection_idx_"..host.address + local dict = cache.get_dict(host.options.shm) + + local ok, err = dict:add(index_key, 0) + if not ok and err ~= "exists" then + log.err("Cannot prepare shared exponential reconnection policy: "..err) + end + + local delay = 0 + dict:incr(index_key, 1) + local index = dict:get(index_key) + if index > 64 then + delay = max_delay + else + delay = math_min(math_pow(index, 2) * base_delay, max_delay) + end + + return delay + end + } +end + +return { + Constant = constant_reconnection_policy, + SharedExponential = shared_exponential_reconnection_policy +} From 65c0109ed8ea2983fd7f74ab673358b5d8666f84 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Mon, 16 Nov 2015 20:33:12 -0800 Subject: [PATCH 26/78] tests(rewrite) load balancing tests (ngx_lua + busted) --- spec/unit/load_balancing_policy_spec.lua | 32 ++++++++++++++++++ src/cassandra/policies/load_balancing.lua | 5 +++ t/00-load-balancing-policies.t | 41 +++++++++++++++++++++++ t/01-cassandra.t | 33 ++++++++++++++++++ 4 files changed, 111 insertions(+) create mode 100644 spec/unit/load_balancing_policy_spec.lua create mode 100644 t/00-load-balancing-policies.t diff --git a/spec/unit/load_balancing_policy_spec.lua b/spec/unit/load_balancing_policy_spec.lua new file mode 100644 index 0000000..c06211b --- /dev/null +++ b/spec/unit/load_balancing_policy_spec.lua @@ -0,0 +1,32 @@ +local load_balancing_policies = require "cassandra.policies.load_balancing" + +describe("Load balancing policies", function() + describe("Shared round robin", function() + local SharedRoundRobin = load_balancing_policies.SharedRoundRobin + local shm = "cassandra" + local hosts = {"127.0.0.1", "127.0.0.2", "127.0.0.3"} + + it("should iterate over the hosts in a round robin fashion", function() + local iter = SharedRoundRobin(shm, hosts) + assert.equal("127.0.0.1", select(2, iter())) + assert.equal("127.0.0.2", select(2, iter())) + assert.equal("127.0.0.3", select(2, iter())) + end) + it("should share its state accros different iterators", function() + local iter1 = SharedRoundRobin(shm, hosts) + local iter2 = SharedRoundRobin(shm, hosts) + assert.equal("127.0.0.1", select(2, iter1())) + assert.equal("127.0.0.2", select(2, iter2())) + assert.equal("127.0.0.3", select(2, iter1())) + end) + it("should be callable in a loop", function() + assert.has_no_errors(function() + local i = 0 + for _, host in SharedRoundRobin(shm, hosts) do + i = i + 1 + end + assert.equal(3, i) + end) + end) + end) +end) diff --git a/src/cassandra/policies/load_balancing.lua b/src/cassandra/policies/load_balancing.lua index 85092f7..5693037 100644 --- a/src/cassandra/policies/load_balancing.lua +++ b/src/cassandra/policies/load_balancing.lua @@ -22,6 +22,11 @@ return { if counter <= n then return mod, hosts[mod] end + + local ok, err = dict:set("rr_plan_index", 0) + if not ok then + log.err("Cannot reset shared round robin load balancing policy: "..err) + end end end } diff --git a/t/00-load-balancing-policies.t b/t/00-load-balancing-policies.t new file mode 100644 index 0000000..78c3dd0 --- /dev/null +++ b/t/00-load-balancing-policies.t @@ -0,0 +1,41 @@ +use Test::Nginx::Socket::Lua; +use Cwd qw(cwd); + +repeat_each(3); + +plan tests => repeat_each() * blocks() * 3; + +my $pwd = cwd(); + +our $HttpConfig = <<_EOC_; + lua_package_path "$pwd/src/?.lua;;"; + lua_shared_dict cassandra 1m; +_EOC_ + +run_tests(); + +__DATA__ + +=== TEST 1: shared round robin +--- http_config eval +"$::HttpConfig" +--- config + location /t { + content_by_lua ' + local iter = require("cassandra.policies.load_balancing").SharedRoundRobin + local shm = "cassandra" + local hosts = {"127.0.0.1", "127.0.0.2", "127.0.0.3"} + + for _, host in iter(shm, hosts) do + ngx.say(host) + end + '; + } +--- request +GET /t +--- response_body +127.0.0.1 +127.0.0.2 +127.0.0.3 +--- no_error_log +[error] diff --git a/t/01-cassandra.t b/t/01-cassandra.t index 3b2b897..4d9a37e 100644 --- a/t/01-cassandra.t +++ b/t/01-cassandra.t @@ -96,3 +96,36 @@ type: ROWS local --- no_error_log [error] + + + +=== TEST 4: session:execute() with request arguments +--- http_config eval +"$::HttpConfig + $::SpawnCluster" +--- config + location /t { + content_by_lua ' + local cassandra = require "cassandra" + local session = cassandra.spawn_session {shm = "cassandra"} + local rows, err = session:execute("SELECT key FROM system.local") + if err then + ngx.log(ngx.ERR, tostring(err)) + ngx.exit(500) + else + ngx.say("type: "..rows.type) + ngx.say("#rows: "..#rows) + for _, row in ipairs(rows) do + ngx.say(row["key"]) + end + end + '; + } +--- request +GET /t +--- response_body +type: ROWS +#rows: 1 +local +--- no_error_log +[error] From a883486b5009f4ed06f1a20959922c23d837eee3 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Tue, 17 Nov 2015 19:02:17 -0800 Subject: [PATCH 27/78] feat(rewrite) manual type inference, set keyspace, test --- Makefile | 2 +- spec/integration/cassandra_spec.lua | 148 +++++++++++++++++++++++- spec/unit/cql_types_buffer_spec.lua | 46 +++++++- src/cassandra.lua | 44 +++++-- src/cassandra/buffer.lua | 77 +++++++----- src/cassandra/cache.lua | 1 - src/cassandra/frame_reader.lua | 17 +++ src/cassandra/options.lua | 14 ++- src/cassandra/policies/reconnection.lua | 3 +- src/cassandra/requests.lua | 63 +++++++++- src/cassandra/types/cql_types.lua | 24 ---- src/cassandra/types/init.lua | 55 +++++++++ src/cassandra/types/options.lua | 9 +- src/cassandra/utils/table.lua | 17 +++ 14 files changed, 435 insertions(+), 85 deletions(-) delete mode 100644 src/cassandra/types/cql_types.lua create mode 100644 src/cassandra/types/init.lua diff --git a/Makefile b/Makefile index 61cd776..598d832 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ dev: done; test: - @busted -v + @busted -v && prove clean: @rm -f luacov.* diff --git a/spec/integration/cassandra_spec.lua b/spec/integration/cassandra_spec.lua index f8a5248..d6a5eaf 100644 --- a/spec/integration/cassandra_spec.lua +++ b/spec/integration/cassandra_spec.lua @@ -9,7 +9,7 @@ local log = require "cassandra.log" -- Define log level for tests log.set_lvl("ERR") -local _shm = "cassandra" +local _shm = "cassandra_specs" local _contact_points = {"127.0.0.1", "127.0.0.2"} describe("spawn cluster", function() @@ -31,8 +31,8 @@ describe("spawn cluster", function() end) it("should retrieve cluster infos in spawned cluster's shm", function() local cache = require "cassandra.cache" - local dict = cache.get_dict(_shm) - local hosts = cache.get_hosts(_shm) + local hosts, err = cache.get_hosts(_shm) + assert.falsy(err) -- index of hosts assert.equal(3, #hosts) -- hosts details @@ -41,9 +41,45 @@ describe("spawn cluster", function() assert.truthy(host_details) end end) + it("should iterate over contact_points to find an entrance into the cluster", function() + local contact_points = {"0.0.0.1", "0.0.0.2", "0.0.0.3"} + contact_points[#contact_points + 1] = _contact_points[1] + + local ok, err = cassandra.spawn_cluster({ + shm = "test", + contact_points = contact_points + }) + assert.falsy(err) + assert.True(ok) + end) + it("should return an error when no contact_point is valid", function() + local contact_points = {"0.0.0.1", "0.0.0.2", "0.0.0.3"} + local ok, err = cassandra.spawn_cluster({ + shm = "test", + contact_points = contact_points + }) + assert.truthy(err) + assert.False(ok) + assert.equal("NoHostAvailableError", err.type) + assert.equal("All hosts tried for query failed. 0.0.0.1: No route to host. 0.0.0.2: No route to host. 0.0.0.3: No route to host.", err.message) + end) + it("should accept a custom port for given hosts", function() + local contact_points = {} + for i, addr in ipairs(_contact_points) do + contact_points[i] = addr..":9043" + end + local ok, err = cassandra.spawn_cluster({ + shm = "test", + contact_points = contact_points + }) + assert.truthy(err) + assert.False(ok) + assert.equal("NoHostAvailableError", err.type) + end) end) describe("spawn session", function() + local session it("should require a 'shm' option", function() assert.has_error(function() cassandra.spawn_session({ @@ -52,10 +88,114 @@ describe("spawn session", function() end, "shm is required for spawning a cluster/session") end) it("should spawn a session", function() - local session, err = cassandra.spawn_session({ + local err + session, err = cassandra.spawn_session({ shm = _shm }) assert.falsy(err) assert.truthy(session) end) + describe(":execute()", function() + describe("ROWS parsing", function() + it("should execute a SELECT query, parsing ROWS", function() + local rows, err = session:execute("SELECT key FROM system.local") + assert.falsy(err) + assert.truthy(rows) + assert.equal("ROWS", rows.type) + assert.equal(1, #rows) + assert.equal("local", rows[1].key) + end) + it("should accept query arguments", function() + local rows, err = session:execute("SELECT key FROM system.local WHERE key = ?", {"local"}) + assert.falsy(err) + assert.truthy(rows) + assert.equal("ROWS", rows.type) + assert.equal(1, #rows) + assert.equal("local", rows[1].key) + end) + end) + describe("SCHEMA_CHANGE/SET_KEYSPACE parsing", function() + local res, err = session:execute [[ + CREATE KEYSPACE IF NOT EXISTS resty_cassandra_spec_parsing + WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor': 1} + ]] + assert.falsy(err) + assert.truthy(res) + assert.equal(0, #res) + assert.equal("SCHEMA_CHANGE", res.type) + assert.equal("CREATED", res.change) + assert.equal("KEYSPACE", res.keyspace) + assert.equal("resty_cassandra_spec_parsing", res.table) + + res, err = session:execute [[USE "resty_cassandra_spec_parsing"]] + assert.falsy(err) + assert.truthy(res) + assert.equal(0, #res) + assert.equal("SET_KEYSPACE", res.type) + assert.equal("resty_cassandra_spec_parsing", res.keyspace) + + res, err = session:execute("DROP KEYSPACE resty_cassandra_spec_parsing") + assert.falsy(err) + assert.truthy(res) + assert.equal(0, #res) + assert.equal("DROPPED", res.change) + end) + end) +end) + +describe("use case", function() + local session + + setup(function() + local err + session, err = cassandra.spawn_session { + shm = _shm + } + assert.falsy(err) + + local _, err = session:execute [[ + CREATE KEYSPACE IF NOT EXISTS resty_cassandra_specs + WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor': 1} + ]] + assert.falsy(err) + + os.execute("sleep 1") + + local _, err = session:execute [[ + CREATE TABLE IF NOT EXISTS resty_cassandra_specs.users( + id uuid PRIMARY KEY, + name varchar, + age int + ) + ]] + assert.falsy(err) + end) + + teardown(function() + local _, err = session:execute("DROP KEYSPACE resty_cassandra_specs") + assert.falsy(err) + + session:close() + end) + + describe(":set_keyspace()", function() + it("should set a session's 'keyspace' option", function() + session:set_keyspace("resty_cassandra_specs") + assert.equal("resty_cassandra_specs", session.options.keyspace) + + local rows, err = session:execute("SELECT * FROM users") + assert.falsy(err) + assert.equal(0, #rows) + end) + end) + + describe(":execute()", function() + it("should accept values to bind", function() + local res, err = session:execute("INSERT INTO users(id, name, age) VALUES(?, ?, ?)", + {cassandra.types.uuid("2644bada-852c-11e3-89fb-e0b9a54a6d93"), "Bob", 42}) + assert.falsy(err) + assert.truthy(res) + assert.equal("VOID", res.type) + end) + end) end) diff --git a/spec/unit/cql_types_buffer_spec.lua b/spec/unit/cql_types_buffer_spec.lua index 5ebe4c8..6355bff 100644 --- a/spec/unit/cql_types_buffer_spec.lua +++ b/spec/unit/cql_types_buffer_spec.lua @@ -1,6 +1,7 @@ local Buffer = require "cassandra.buffer" local CONSTS = require "cassandra.constants" -local CQL_TYPES = require "cassandra.types.cql_types" +local types = require "cassandra.types" +local CQL_TYPES = types.cql_types for _, protocol_version in ipairs(CONSTS.SUPPORTED_PROTOCOL_VERSIONS) do @@ -31,6 +32,24 @@ describe("CQL Types protocol v"..protocol_version, function() end end end) + + describe("manual type infering", function() + it("should be possible to infer the type of a value through helper methods", function() + for _, fixture in ipairs(fixture_values) do + local infered_value = types[fixture_type](fixture) + local buf = Buffer(protocol_version) + buf:write_cql_value(infered_value) + buf:reset() + + local decoded = buf:read_cql_value({type_id = CQL_TYPES[fixture_type]}) + if type(fixture) == "table" then + assert.same(fixture, decoded) + else + assert.equal(fixture, decoded) + end + end + end) + end) end it("[map] should be bufferable", function() @@ -44,7 +63,7 @@ describe("CQL Types protocol v"..protocol_version, function() local buf = Buffer(protocol_version) buf:write_cql_map(fixture.value) buf:reset() - local decoded = buf:read_cql_map({{id = fixture.key_type}, {id = fixture.value_type}}) + local decoded = buf:read_cql_map({{type_id = fixture.key_type}, {type_id = fixture.value_type}}) assert.same(fixture.value, decoded) end end) @@ -59,10 +78,31 @@ describe("CQL Types protocol v"..protocol_version, function() local buf = Buffer(protocol_version) buf:write_cql_set(fixture.value) buf:reset() - local decoded = buf:read_cql_set({id = fixture.value_type}) + local decoded = buf:read_cql_set({type_id = fixture.value_type}) assert.same(fixture.value, decoded) end end) + + describe("write_cql_values", function() + it("should loop over given values and infer their types", function() + local values = { + 42, + {"hello", "world"}, + {hello = "world"}, + "hello world" + } + + local buf = Buffer(protocol_version) + buf:write_cql_values(values) + buf:reset() + assert.equal(#values, buf:read_short()) + assert.equal(values[1], buf:read_cql_int()) + assert.same(values[2], buf:read_cql_set({type_id = CQL_TYPES.text})) + assert.same(values[3], buf:read_cql_map({{type_id = CQL_TYPES.text}, {type_id = CQL_TYPES.text}})) + assert.same(values[4], buf:read_cql_raw()) + end) + end) end) end + diff --git a/src/cassandra.lua b/src/cassandra.lua index b86e5ee..a3843b7 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -1,11 +1,13 @@ local log = require "cassandra.log" local opts = require "cassandra.options" +local types = require "cassandra.types" local cache = require "cassandra.cache" local Object = require "cassandra.classic" local CONSTS = require "cassandra.constants" local Errors = require "cassandra.errors" local Requests = require "cassandra.requests" local time_utils = require "cassandra.utils.time" +local table_utils = require "cassandra.utils.table" local string_utils = require "cassandra.utils.string" local frame_header = require "cassandra.types.frame_header" local frame_reader = require "cassandra.frame_reader" @@ -13,6 +15,7 @@ local frame_reader = require "cassandra.frame_reader" local table_insert = table.insert local string_find = string.find +local CQL_Errors = frame_reader.errors local FrameReader = frame_reader.FrameReader local FrameHeader = frame_header.FrameHeader @@ -124,6 +127,13 @@ local function startup(self) return self:send(startup_req) end +local function change_keyspace(self) + log.info("Keyspace request. Using keyspace: "..self.options.keyspace) + + local keyspace_req = Requests.KeyspaceRequest(self.options.keyspace) + return self:send(keyspace_req) +end + function Host:connect() log.info("Connecting to "..self.address) @@ -163,6 +173,14 @@ function Host:connect() return false, err elseif res.ready then log.info("Host at "..self.address.." is ready with protocol v"..self.protocol_version) + + if self.options.keyspace ~= nil then + local _, err = change_keyspace(self) + if err then + return false, err + end + end + return true end end @@ -299,7 +317,7 @@ function RequestHandler:get_next_coordinator() local iter = self.options.policies.load_balancing local hosts, cache_err = cache.get_hosts(self.options.shm) - if err then + if cache_err then return nil, cache_err end @@ -351,7 +369,7 @@ function RequestHandler:send() -- Success! Make sure to re-up node in case it was marked as DOWN local ok, cache_err = coordinator:set_up() - if err then + if not ok then return nil, cache_err end @@ -362,7 +380,7 @@ function RequestHandler:handle_error(err) local retry_policy = self.options.policies.retry local decision = retry_policy.decisions.throw - if err.type == "SocketError" or err.type == "TimeoutError" then + if err.type == "SocketError" then -- host seems unhealthy local ok, cache_err = self.coordinator:set_down() if not ok then @@ -370,6 +388,10 @@ function RequestHandler:handle_error(err) end -- always retry, another node will be picked return self:retry() + elseif err.type == "TimeoutError" then + if self.options.query_options.retry_on_timeout then + return self:retry() + end elseif err.type == "ResponseError" then local request_infos = { handler = self, @@ -422,13 +444,19 @@ function Session:new(options) return setmetatable(s, {__index = self}) end +function Session:execute(query, args, options) + local q_options = table_utils.deep_copy(self.options) + q_options.query_options = table_utils.extend_table(q_options.query_options, options) -function Session:execute(query) - local query_request = Requests.QueryRequest(query) - local request_handler = RequestHandler:new(query_request, self.options) + local query_request = Requests.QueryRequest(query, args, options) + local request_handler = RequestHandler:new(query_request, q_options) return request_handler:send() end +function Session:set_keyspace(keyspace) + self.options.keyspace = keyspace +end + function Session:close() if self.coordinator ~= nil then return self.coordinator:close() @@ -503,7 +531,7 @@ function Cassandra.refresh_hosts(contact_points_hosts, options) for addr, host in pairs(hosts) do table_insert(addresses, addr) local ok, cache_err = cache.set_host(options.shm, addr, host) - if err then + if not ok then return false, cache_err end end @@ -523,4 +551,6 @@ function Cassandra.spawn_cluster(options) return Cassandra.refresh_hosts(contact_points_hosts, options) end +Cassandra.types = types + return Cassandra diff --git a/src/cassandra/buffer.lua b/src/cassandra/buffer.lua index 89dbfae..dc01ef4 100644 --- a/src/cassandra/buffer.lua +++ b/src/cassandra/buffer.lua @@ -1,7 +1,10 @@ local Buffer = require "cassandra.utils.buffer" -local CQL_TYPES = require "cassandra.types.cql_types" local t_utils = require "cassandra.utils.table" +local types = require "cassandra.types" +local cql_types = types.cql_types + local math_floor = math.floor +local type = type --- Frame types -- @section frame_types @@ -40,27 +43,27 @@ end local CQL_DECODERS = { -- custom = 0x00, - [CQL_TYPES.ascii] = "raw", - -- [CQL_TYPES.bigint] = "bigint", - [CQL_TYPES.blob] = "raw", - [CQL_TYPES.boolean] = "boolean", - -- [CQL_TYPES.counter] = "counter", + [cql_types.ascii] = "raw", + -- [cql_types.bigint] = "bigint", + [cql_types.blob] = "raw", + [cql_types.boolean] = "boolean", + -- [cql_types.counter] = "counter", -- decimal 0x06 - -- [CQL_TYPES.double] = "double", - -- [CQL_TYPES.float] = "float", - [CQL_TYPES.inet] = "inet", - [CQL_TYPES.int] = "int", - [CQL_TYPES.text] = "raw", - [CQL_TYPES.list] = "set", - [CQL_TYPES.map] = "map", - [CQL_TYPES.set] = "set", - [CQL_TYPES.uuid] = "uuid", - -- [CQL_TYPES.timestamp] = "timestamp", - [CQL_TYPES.varchar] = "raw", - -- [CQL_TYPES.varint] = "varint", - -- [CQL_TYPES.timeuuid] = "timeuuid", - -- [CQL_TYPES.udt] = "udt", - -- [CQL_TYPES.tuple] = "tuple" + -- [cql_types.double] = "double", + -- [cql_types.float] = "float", + [cql_types.inet] = "inet", + [cql_types.int] = "int", + [cql_types.text] = "raw", + [cql_types.list] = "set", + [cql_types.map] = "map", + [cql_types.set] = "set", + [cql_types.uuid] = "uuid", + -- [cql_types.timestamp] = "timestamp", + [cql_types.varchar] = "raw", + -- [cql_types.varint] = "varint", + -- [cql_types.timeuuid] = "timeuuid", + -- [cql_types.udt] = "udt", + -- [cql_types.tuple] = "tuple" } for _, cql_decoder in pairs(CQL_DECODERS) do @@ -80,22 +83,27 @@ for _, cql_decoder in pairs(CQL_DECODERS) do end end -function Buffer:repr_cql_value(value, assumed_type) +function Buffer:repr_cql_value(value) local infered_type local lua_type = type(value) - if assumed_type then - infered_type = assumed_type - elseif lua_type == "number" and math_floor(value) == value then - infered_type = CQL_TYPES.int + if lua_type == "number" then + if math_floor(value) == value then + infered_type = cql_types.int + else + --infered_type = cql_types.float + end elseif lua_type == "table" then if t_utils.is_array(value) then - infered_type = CQL_TYPES.set + infered_type = cql_types.set + elseif value.value ~= nil and value.type_id ~= nil then + infered_type = value.type_id + value = value.value else - infered_type = CQL_TYPES.map + infered_type = cql_types.map end else - infered_type = CQL_TYPES.varchar + infered_type = cql_types.varchar end local encoder = "repr_cql_"..CQL_DECODERS[infered_type] @@ -107,8 +115,15 @@ function Buffer:write_cql_value(...) end function Buffer:read_cql_value(assumed_type) - local decoder = "read_cql_"..CQL_DECODERS[assumed_type.id] - return Buffer[decoder](self, assumed_type.value) + local decoder = "read_cql_"..CQL_DECODERS[assumed_type.type_id] + return Buffer[decoder](self, assumed_type.value_type_id) +end + +function Buffer:write_cql_values(values) + self:write_short(#values) + for _, value in ipairs(values) do + self:write_cql_value(value) + end end return Buffer diff --git a/src/cassandra/cache.lua b/src/cassandra/cache.lua index 13bee2b..95ce369 100644 --- a/src/cassandra/cache.lua +++ b/src/cassandra/cache.lua @@ -1,5 +1,4 @@ local json = require "cjson" -local log = require "cassandra.log" local string_utils = require "cassandra.utils.string" local table_concat = table.concat local in_ngx = ngx ~= nil diff --git a/src/cassandra/frame_reader.lua b/src/cassandra/frame_reader.lua index 6b6b455..e86f2b2 100644 --- a/src/cassandra/frame_reader.lua +++ b/src/cassandra/frame_reader.lua @@ -99,6 +99,9 @@ local function parse_metadata(buffer) end local RESULT_PARSERS = { + [RESULT_KINDS.VOID] = function() + return {type = "VOID"} + end, [RESULT_KINDS.ROWS] = function(buffer) local metadata = parse_metadata(buffer) local columns = metadata.columns @@ -121,6 +124,20 @@ local RESULT_PARSERS = { end return rows + end, + [RESULT_KINDS.SET_KEYSPACE] = function(buffer) + return { + type = "SET_KEYSPACE", + keyspace = buffer:read_string() + } + end, + [RESULT_KINDS.SCHEMA_CHANGE] = function(buffer) + return { + type = "SCHEMA_CHANGE", + change = buffer:read_string(), + keyspace = buffer:read_string(), + table = buffer:read_string() + } end } diff --git a/src/cassandra/options.lua b/src/cassandra/options.lua index 8ea58cc..e2de0a1 100644 --- a/src/cassandra/options.lua +++ b/src/cassandra/options.lua @@ -1,3 +1,4 @@ +local types = require "cassandra.types" local utils = require "cassandra.utils.table" --- Defaults @@ -6,12 +7,21 @@ local utils = require "cassandra.utils.table" local DEFAULTS = { shm = nil, -- required contact_points = {}, + keyspace = nil, -- stub policies = { address_resolution = require "cassandra.policies.address_resolution", load_balancing = require("cassandra.policies.load_balancing").SharedRoundRobin, retry = require("cassandra.policies.retry"), reconnection = require("cassandra.policies.reconnection").SharedExponential(1000, 10 * 60 * 1000) }, + query_options = { + consistency = types.consistencies.one, + serial_consistency = types.consistencies.serial, + page_size = 5000, + paging_state = nil, -- stub + prepare = false, + retry_on_timeout = true + }, protocol_options = { default_port = 9042 }, @@ -22,8 +32,6 @@ local DEFAULTS = { } local function parse_session(options) - if options == nil then options = {} end - utils.extend_table(DEFAULTS, options) --if type(options.keyspace) ~= "string" then @@ -41,8 +49,6 @@ local function parse_session(options) end local function parse_cluster(options) - if options == nil then options = {} end - parse_session(options) if type(options.contact_points) ~= "table" then diff --git a/src/cassandra/policies/reconnection.lua b/src/cassandra/policies/reconnection.lua index d5a5161..9f9ad44 100644 --- a/src/cassandra/policies/reconnection.lua +++ b/src/cassandra/policies/reconnection.lua @@ -1,3 +1,4 @@ +local log = require "cassandra.log" local cache = require "cassandra.cache" local math_pow = math.pow local math_min = math.min @@ -31,7 +32,7 @@ local function shared_exponential_reconnection_policy(base_delay, max_delay) log.err("Cannot prepare shared exponential reconnection policy: "..err) end - local delay = 0 + local delay dict:incr(index_key, 1) local index = dict:get(index_key) if index > 64 then diff --git a/src/cassandra/requests.lua b/src/cassandra/requests.lua index fbe6b47..b6c94f7 100644 --- a/src/cassandra/requests.lua +++ b/src/cassandra/requests.lua @@ -1,3 +1,5 @@ +local bit = require "cassandra.utils.bit" +local types = require "cassandra.types" local CONSTS = require "cassandra.constants" local Object = require "cassandra.classic" local Buffer = require "cassandra.buffer" @@ -6,6 +8,22 @@ local frame_header = require "cassandra.types.frame_header" local op_codes = frame_header.op_codes local FrameHeader = frame_header.FrameHeader +local string_format = string.format + +--- Query Flags +-- @section query_flags + +local query_flags = { + values = 0x01, + skip_metadata = 0x02, -- not implemented + page_size = 0x04, + paging_state = 0x08, + -- 0x09 + serial_consistency = 0x10, + default_timestamp = 0x20, + named_values = 0x40 +} + --- Request -- @section request @@ -15,7 +33,7 @@ function Request:new(op_code) self.version = nil self.flags = 0 self.op_code = op_code - self.frameBody = Buffer() -- no version + self.frameBody = Buffer() -- no version yet at this point self.built = false Request.super.new(self) @@ -69,7 +87,7 @@ local QueryRequest = Request:extend() function QueryRequest:new(query, params, options) self.query = query self.params = params - self.options = options + self.options = options or {} QueryRequest.super.new(self, op_codes.QUERY) end @@ -78,12 +96,47 @@ function QueryRequest:build() -- [...][][][] -- v3: -- [[name_1]...[name_n]][][][][] + if self.options.consistency == nil then + self.options.consistency = types.consistencies.one + end + + local flags = 0x00 + local flags_buffer = Buffer(self.version) + if self.params ~= nil then + flags = bit.bor(flags, query_flags.values) + flags_buffer:write_cql_values(self.params) + end + if self.options.page_size ~= nil then + flags = bit.bor(flags, query_flags.page_size) + flags_buffer:write_int(self.options.page_size) + end + if self.options.paging_state ~= nil then + flags = bit.bor(flags, query_flags.paging_state) + flags_buffer:write_bytes(self.options.paging_state) + end + if self.options.serial_consistency ~= nil then + flags = bit.bor(flags, query_flags.serial_consistency) + flags_buffer:write_short(self.options.serial_consistency) + end + self.frameBody:write_long_string(self.query) - self.frameBody:write_short(0x0001) -- @TODO support consistency_level - self.frameBody:write_byte(0) -- @TODO support query flags + self.frameBody:write_short(self.options.consistency) + self.frameBody:write_byte(flags) + self.frameBody:write(flags_buffer:dump()) +end + +--- KeyspaceRequest +-- @section keyspace_request + +local KeyspaceRequest = QueryRequest:extend() + +function KeyspaceRequest:new(keyspace) + local query = string_format([[USE "%s"]], keyspace) + KeyspaceRequest.super.new(self, query) end return { StartupRequest = StartupRequest, - QueryRequest = QueryRequest + QueryRequest = QueryRequest, + KeyspaceRequest = KeyspaceRequest } diff --git a/src/cassandra/types/cql_types.lua b/src/cassandra/types/cql_types.lua deleted file mode 100644 index 1f5564b..0000000 --- a/src/cassandra/types/cql_types.lua +++ /dev/null @@ -1,24 +0,0 @@ -return { - custom = 0x00, - ascii = 0x01, - bigint = 0x02, - blob = 0x03, - boolean = 0x04, - counter = 0x05, - decimal = 0x06, - double = 0x07, - float = 0x08, - int = 0x09, - text = 0x0A, - timestamp = 0x0B, - uuid = 0x0C, - varchar = 0x0D, - varint = 0x0E, - timeuuid = 0x0F, - inet = 0x10, - list = 0x20, - map = 0x21, - set = 0x22, - udt = 0x30, - tuple = 0x31 -} diff --git a/src/cassandra/types/init.lua b/src/cassandra/types/init.lua new file mode 100644 index 0000000..28e1ddf --- /dev/null +++ b/src/cassandra/types/init.lua @@ -0,0 +1,55 @@ +local cql_types = { + custom = 0x00, + ascii = 0x01, + bigint = 0x02, + blob = 0x03, + boolean = 0x04, + counter = 0x05, + decimal = 0x06, + double = 0x07, + float = 0x08, + int = 0x09, + text = 0x0A, + timestamp = 0x0B, + uuid = 0x0C, + varchar = 0x0D, + varint = 0x0E, + timeuuid = 0x0F, + inet = 0x10, + list = 0x20, + map = 0x21, + set = 0x22, + udt = 0x30, + tuple = 0x31 +} + +local consistencies = { + any = 0X0000, + one = 0X0001, + two = 0X0002, + three = 0X0003, + quorum = 0X0004, + all = 0X0005, + local_quorum = 0X0006, + each_quorum = 0X0007, + serial = 0X0008, + local_serial = 0X0009, + local_one = 0X000a +} + +local types_mt = {} + +function types_mt:__index(key) + if cql_types[key] ~= nil then + return function(value) + return {value = value, type_id = cql_types[key]} + end + end + + return rawget(self, key) +end + +return setmetatable({ + cql_types = cql_types, + consistencies = consistencies +}, types_mt) diff --git a/src/cassandra/types/options.lua b/src/cassandra/types/options.lua index 4ecc29e..5064c3d 100644 --- a/src/cassandra/types/options.lua +++ b/src/cassandra/types/options.lua @@ -1,16 +1,17 @@ -local CQL_TYPES = require "cassandra.types.cql_types" +local types = require "cassandra.types" + return { read = function(buffer) local type_id = buffer:read_short() local type_value - if type_id == CQL_TYPES.set then + if type_id == types.cql_types.set then type_value = buffer:read_options() - elseif type_id == CQL_TYPES.map then + elseif type_id == types.cql_types.map then type_value = {buffer:read_options(), buffer:read_options()} end -- @TODO support non-native types (custom, map, list, set, UDT, tuple) - return {id = type_id, value = type_value} + return {type_id = type_id, value_type_id = type_value} end } diff --git a/src/cassandra/utils/table.lua b/src/cassandra/utils/table.lua index a4a88ca..9e51a1b 100644 --- a/src/cassandra/utils/table.lua +++ b/src/cassandra/utils/table.lua @@ -3,6 +3,7 @@ local CONSTS = require "cassandra.constants" local _M = {} function _M.extend_table(defaults, values) + if values == nil then values = {} end for k in pairs(defaults) do if values[k] == nil then values[k] = defaults[k] @@ -11,6 +12,22 @@ function _M.extend_table(defaults, values) _M.extend_table(defaults[k], values[k]) end end + + return values +end + +function _M.deep_copy(orig) + local copy + if type(orig) == "table" then + copy = {} + for orig_key, orig_value in next, orig, nil do + copy[_M.deep_copy(orig_key)] = _M.deep_copy(orig_value) + end + setmetatable(copy, _M.deep_copy(getmetatable(orig))) + else + copy = orig + end + return copy end function _M.is_array(t) From 9d87dd52c9373d23496bbfdee8b53676359bf788 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Tue, 17 Nov 2015 20:56:49 -0800 Subject: [PATCH 28/78] fix(tests) include init.lua in ngx_lua tests' package.path --- t/00-load-balancing-policies.t | 2 +- t/01-cassandra.t | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/t/00-load-balancing-policies.t b/t/00-load-balancing-policies.t index 78c3dd0..91a791e 100644 --- a/t/00-load-balancing-policies.t +++ b/t/00-load-balancing-policies.t @@ -8,7 +8,7 @@ plan tests => repeat_each() * blocks() * 3; my $pwd = cwd(); our $HttpConfig = <<_EOC_; - lua_package_path "$pwd/src/?.lua;;"; + lua_package_path "$pwd/src/?.lua;$pwd/src/?/init.lua;;"; lua_shared_dict cassandra 1m; _EOC_ diff --git a/t/01-cassandra.t b/t/01-cassandra.t index 4d9a37e..9e5f820 100644 --- a/t/01-cassandra.t +++ b/t/01-cassandra.t @@ -8,7 +8,7 @@ plan tests => repeat_each() * blocks() * 3; my $pwd = cwd(); our $HttpConfig = <<_EOC_; - lua_package_path "$pwd/src/?.lua;;"; + lua_package_path "$pwd/src/?.lua;$pwd/src/?/init.lua;;"; _EOC_ our $SpawnCluster = <<_EOC_; From 4d022087dc6ca8e8ce04417389830bbc2747f65a Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Tue, 17 Nov 2015 21:35:51 -0800 Subject: [PATCH 29/78] cleanup(rewrite) gathering CQL constants in cassandra.types --- spec/unit/cql_types_buffer_spec.lua | 3 +- spec/unit/requests_spec.lua | 9 ++- src/cassandra.lua | 36 +++++++--- src/cassandra/frame_reader.lua | 55 +++------------ src/cassandra/requests.lua | 36 +++++----- src/cassandra/types/frame_header.lua | 44 ++---------- src/cassandra/types/init.lua | 102 ++++++++++++++++++++------- 7 files changed, 142 insertions(+), 143 deletions(-) diff --git a/spec/unit/cql_types_buffer_spec.lua b/spec/unit/cql_types_buffer_spec.lua index 6355bff..7bd473a 100644 --- a/spec/unit/cql_types_buffer_spec.lua +++ b/spec/unit/cql_types_buffer_spec.lua @@ -1,3 +1,4 @@ +local cassandra = require "cassandra" local Buffer = require "cassandra.buffer" local CONSTS = require "cassandra.constants" local types = require "cassandra.types" @@ -36,7 +37,7 @@ describe("CQL Types protocol v"..protocol_version, function() describe("manual type infering", function() it("should be possible to infer the type of a value through helper methods", function() for _, fixture in ipairs(fixture_values) do - local infered_value = types[fixture_type](fixture) + local infered_value = cassandra.types[fixture_type](fixture) local buf = Buffer(protocol_version) buf:write_cql_value(infered_value) buf:reset() diff --git a/spec/unit/requests_spec.lua b/spec/unit/requests_spec.lua index 8db9ab2..7f01b75 100644 --- a/spec/unit/requests_spec.lua +++ b/spec/unit/requests_spec.lua @@ -1,9 +1,8 @@ -local requests = require "cassandra.requests" +local types = require "cassandra.types" local Buffer = require "cassandra.buffer" +local requests = require "cassandra.requests" local frame_header = require "cassandra.types.frame_header" -local op_codes = frame_header.op_codes - describe("Requests", function() describe("StartupRequest", function() it("should write a startup request", function() @@ -15,7 +14,7 @@ describe("Requests", function() assert.equal(0x03, full_buffer:read_byte()) assert.equal(0, full_buffer:read_byte()) assert.equal(0, full_buffer:read_short()) - assert.equal(op_codes.STARTUP, full_buffer:read_byte()) + assert.equal(types.OP_CODES.STARTUP, full_buffer:read_byte()) assert.equal(22, full_buffer:read_int()) assert.same({CQL_VERSION = "3.0.0"}, full_buffer:read_string_map()) end) @@ -30,7 +29,7 @@ describe("Requests", function() assert.equal(0x02, full_buffer:read_byte()) assert.equal(0, full_buffer:read_byte()) assert.equal(0, full_buffer:read_byte()) - assert.equal(op_codes.STARTUP, full_buffer:read_byte()) + assert.equal(types.OP_CODES.STARTUP, full_buffer:read_byte()) assert.equal(22, full_buffer:read_int()) assert.same({CQL_VERSION = "3.0.0"}, full_buffer:read_string_map()) end) diff --git a/src/cassandra.lua b/src/cassandra.lua index a3843b7..db20617 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -9,15 +9,12 @@ local Requests = require "cassandra.requests" local time_utils = require "cassandra.utils.time" local table_utils = require "cassandra.utils.table" local string_utils = require "cassandra.utils.string" -local frame_header = require "cassandra.types.frame_header" -local frame_reader = require "cassandra.frame_reader" +local FrameHeader = require "cassandra.types.frame_header" +local FrameReader = require "cassandra.frame_reader" local table_insert = table.insert local string_find = string.find - -local CQL_Errors = frame_reader.errors -local FrameReader = frame_reader.FrameReader -local FrameHeader = frame_header.FrameHeader +local CQL_Errors = types.ERRORS --- Host -- A connection to a single host. @@ -501,7 +498,8 @@ function Cassandra.refresh_hosts(contact_points_hosts, options) rack = row["rack"], cassandra_version = row["release_version"], protocol_versiom = row["native_protocol_version"], - unhealthy_at = 0 + unhealthy_at = 0, + reconnection_delay = 0 } hosts[address] = local_host log.info("Local info retrieved") @@ -519,7 +517,8 @@ function Cassandra.refresh_hosts(contact_points_hosts, options) rack = row["rack"], cassandra_version = row["release_version"], protocol_version = local_host.native_protocol_version, - unhealthy_at = 0 + unhealthy_at = 0, + reconnection_delay = 0 } end log.info("Peers info retrieved") @@ -551,6 +550,25 @@ function Cassandra.spawn_cluster(options) return Cassandra.refresh_hosts(contact_points_hosts, options) end -Cassandra.types = types +--- CQL types inferers +-- @section + +local CQL_TYPES = types.cql_types + +local types_mt = {} + +function types_mt:__index(key) + if CQL_TYPES[key] ~= nil then + return function(value) + return {value = value, type_id = CQL_TYPES[key]} + end + end + + return rawget(self, key) +end + +Cassandra.types = setmetatable({}, types_mt) + +Cassandra.consistencies = types.consistencies return Cassandra diff --git a/src/cassandra/frame_reader.lua b/src/cassandra/frame_reader.lua index e86f2b2..732272c 100644 --- a/src/cassandra/frame_reader.lua +++ b/src/cassandra/frame_reader.lua @@ -1,49 +1,13 @@ +local bit = require "cassandra.utils.bit" +local types = require "cassandra.types" local Object = require "cassandra.classic" local Buffer = require "cassandra.buffer" local errors = require "cassandra.errors" -local frame_header = require "cassandra.types.frame_header" -local bit = require "cassandra.utils.bit" -local op_codes = frame_header.op_codes +local OP_CODES = types.OP_CODES --- CONST -- @section constants -local ERRORS = { - SERVER = 0x0000, - PROTOCOL = 0x000A, - BAD_CREDENTIALS = 0x0100, - UNAVAILABLE_EXCEPTION = 0x1000, - OVERLOADED = 0x1001, - IS_BOOTSTRAPPING = 0x1002, - TRUNCATE_ERROR = 0x1003, - WRITE_TIMEOUT = 0x1100, - READ_TIMEOUT = 0x1200, - SYNTAX_ERROR = 0x2000, - UNAUTHORIZED = 0x2100, - INVALID = 0x2200, - CONFIG_ERROR = 0x2300, - ALREADY_EXISTS = 0x2400, - UNPREPARED = 0x2500 -} - -local ERRORS_TRANSLATION = { - [ERRORS.SERVER] = "Server error", - [ERRORS.PROTOCOL] = "Protocol error", - [ERRORS.BAD_CREDENTIALS] = "Bad credentials", - [ERRORS.UNAVAILABLE_EXCEPTION] = "Unavailable exception", - [ERRORS.OVERLOADED] = "Overloaded", - [ERRORS.IS_BOOTSTRAPPING] = "Is bootstrapping", - [ERRORS.TRUNCATE_ERROR] = "Truncate error", - [ERRORS.WRITE_TIMEOUT] = "Write timeout", - [ERRORS.READ_TIMEOUT] = "Read timeout", - [ERRORS.SYNTAX_ERROR] = "Syntaxe rror", - [ERRORS.UNAUTHORIZED] = "Unauthorized", - [ERRORS.INVALID] = "Invalid", - [ERRORS.CONFIG_ERROR] = "Config error", - [ERRORS.ALREADY_EXISTS] = "Already exists", - [ERRORS.UNPREPARED] = "Unprepared" -} - local RESULT_KINDS = { VOID = 0x01, ROWS = 0x02, @@ -154,7 +118,7 @@ end local function parse_error(frameBody) local code = frameBody:read_int() local message = frameBody:read_string() - local code_translation = ERRORS_TRANSLATION[code] + local code_translation = types.ERRORS_TRANSLATIONS[code] return errors.ResponseError(code, code_translation, message) end @@ -176,16 +140,13 @@ function FrameReader:parse() end -- Parse frame depending on op_code - if op_code == op_codes.ERROR then + if op_code == OP_CODES.ERROR then return nil, parse_error(self.frameBody) - elseif op_code == op_codes.READY then + elseif op_code == OP_CODES.READY then return parse_ready(self.frameBody) - elseif op_code == op_codes.RESULT then + elseif op_code == OP_CODES.RESULT then return parse_result(self.frameBody) end end -return { - FrameReader = FrameReader, - errors = ERRORS -} +return FrameReader diff --git a/src/cassandra/requests.lua b/src/cassandra/requests.lua index b6c94f7..2daf0fe 100644 --- a/src/cassandra/requests.lua +++ b/src/cassandra/requests.lua @@ -3,11 +3,9 @@ local types = require "cassandra.types" local CONSTS = require "cassandra.constants" local Object = require "cassandra.classic" local Buffer = require "cassandra.buffer" -local frame_header = require "cassandra.types.frame_header" - -local op_codes = frame_header.op_codes -local FrameHeader = frame_header.FrameHeader +local FrameHeader = require "cassandra.types.frame_header" +local OP_CODES = types.OP_CODES local string_format = string.format --- Query Flags @@ -30,10 +28,10 @@ local query_flags = { local Request = Object:extend() function Request:new(op_code) - self.version = nil + self.version = nil -- no version yet at this point self.flags = 0 self.op_code = op_code - self.frameBody = Buffer() -- no version yet at this point + self.frame_body = Buffer() -- no version yet at this point self.built = false Request.super.new(self) @@ -41,7 +39,7 @@ end function Request:set_version(version) self.version = version - self.frameBody.version = version + self.frame_body.version = version end function Request:build() @@ -49,17 +47,17 @@ function Request:build() end function Request:get_full_frame() - if not self.op_code then error("Request#write() has no op_code attribute") end - if not self.version then error("Request#write() has no version attribute") end + if not self.op_code then error("Request#get_full_frame() has no op_code attribute") end + if not self.version then error("Request#get_full_frame() has no version attribute") end if not self.built then self:build() self.built = true end - local frameHeader = FrameHeader(self.version, self.flags, self.op_code, self.frameBody.len) - local header = frameHeader:dump() - local body = self.frameBody:dump() + local frame_header = FrameHeader(self.version, self.flags, self.op_code, self.frame_body.len) + local header = frame_header:dump() + local body = self.frame_body:dump() return header..body end @@ -70,11 +68,11 @@ end local StartupRequest = Request:extend() function StartupRequest:new() - StartupRequest.super.new(self, op_codes.STARTUP) + StartupRequest.super.new(self, OP_CODES.STARTUP) end function StartupRequest:build() - self.frameBody:write_string_map({ + self.frame_body:write_string_map({ CQL_VERSION = CONSTS.CQL_VERSION }) end @@ -88,7 +86,7 @@ function QueryRequest:new(query, params, options) self.query = query self.params = params self.options = options or {} - QueryRequest.super.new(self, op_codes.QUERY) + QueryRequest.super.new(self, OP_CODES.QUERY) end function QueryRequest:build() @@ -119,10 +117,10 @@ function QueryRequest:build() flags_buffer:write_short(self.options.serial_consistency) end - self.frameBody:write_long_string(self.query) - self.frameBody:write_short(self.options.consistency) - self.frameBody:write_byte(flags) - self.frameBody:write(flags_buffer:dump()) + self.frame_body:write_long_string(self.query) + self.frame_body:write_short(self.options.consistency) + self.frame_body:write_byte(flags) + self.frame_body:write(flags_buffer:dump()) end --- KeyspaceRequest diff --git a/src/cassandra/types/frame_header.lua b/src/cassandra/types/frame_header.lua index 383c9d5..0391db1 100644 --- a/src/cassandra/types/frame_header.lua +++ b/src/cassandra/types/frame_header.lua @@ -1,6 +1,6 @@ -local utils = require "cassandra.utils.table" local bit = require "cassandra.utils.bit" local Buffer = require "cassandra.buffer" +local table_utils = require "cassandra.utils.table" --- CONST -- @section constants @@ -16,35 +16,7 @@ local VERSION_CODES = { } } -setmetatable(VERSION_CODES, utils.const_mt) - -local FLAGS = { - COMPRESSION = 0x01, -- not implemented - TRACING = 0x02 -} - --- when we'll support protocol v4, other --- flags will be added. --- setmetatable(FLAGS, utils.const_mt) - -local OP_CODES = { - ERROR = 0x00, - STARTUP = 0x01, - READY = 0x02, - AUTHENTICATE = 0x03, - OPTIONS = 0x05, - SUPPORTED = 0x06, - QUERY = 0x07, - RESULT = 0x08, - PREPARE = 0x09, - EXECUTE = 0x0A, - REGISTER = 0x0B, - EVENT = 0x0C, - BATCH = 0x0D, - AUTH_CHALLENGE = 0x0E, - AUTH_RESPONSE = 0x0F, - AUTH_SUCCESS = 0x10 -} +setmetatable(VERSION_CODES, table_utils.const_mt) --- FrameHeader -- @section FrameHeader @@ -54,7 +26,7 @@ local FrameHeader = Buffer:extend() function FrameHeader:new(version, flags, op_code, body_length) self.flags = flags and flags or 0 self.op_code = op_code - self.stream_id = 0 -- @TODO support streaming + self.stream_id = 0 self.body_length = body_length self.super.new(self, version) @@ -62,7 +34,7 @@ end function FrameHeader:dump() FrameHeader.super.write_byte(self, VERSION_CODES:get("REQUEST", self.version)) - FrameHeader.super.write_byte(self, self.flags) -- @TODO find a more secure way + FrameHeader.super.write_byte(self, self.flags) if self.version < 3 then FrameHeader.super.write_byte(self, self.stream_id) @@ -70,7 +42,7 @@ function FrameHeader:dump() FrameHeader.super.write_short(self, self.stream_id) end - FrameHeader.super.write_byte(self, self.op_code) -- @TODO find a more secure way + FrameHeader.super.write_byte(self, self.op_code) FrameHeader.super.write_int(self, self.body_length) return FrameHeader.super.dump(self) @@ -107,8 +79,4 @@ function FrameHeader.from_raw_bytes(version_byte, raw_bytes) return FrameHeader(version, flags, op_code, body_length) end -return { - op_codes = OP_CODES, - flags = FLAGS, - FrameHeader = FrameHeader -} +return FrameHeader diff --git a/src/cassandra/types/init.lua b/src/cassandra/types/init.lua index 28e1ddf..51061d8 100644 --- a/src/cassandra/types/init.lua +++ b/src/cassandra/types/init.lua @@ -1,3 +1,27 @@ +local QUERY_FLAGS = { + COMPRESSION = 0x01, -- not implemented + TRACING = 0x02 +} + +local OP_CODES = { + ERROR = 0x00, + STARTUP = 0x01, + READY = 0x02, + AUTHENTICATE = 0x03, + OPTIONS = 0x05, + SUPPORTED = 0x06, + QUERY = 0x07, + RESULT = 0x08, + PREPARE = 0x09, + EXECUTE = 0x0A, + REGISTER = 0x0B, + EVENT = 0x0C, + BATCH = 0x0D, + AUTH_CHALLENGE = 0x0E, + AUTH_RESPONSE = 0x0F, + AUTH_SUCCESS = 0x10 +} + local cql_types = { custom = 0x00, ascii = 0x01, @@ -24,32 +48,62 @@ local cql_types = { } local consistencies = { - any = 0X0000, - one = 0X0001, - two = 0X0002, - three = 0X0003, - quorum = 0X0004, - all = 0X0005, - local_quorum = 0X0006, - each_quorum = 0X0007, - serial = 0X0008, - local_serial = 0X0009, - local_one = 0X000a + any = 0x0000, + one = 0x0001, + two = 0x0002, + three = 0x0003, + quorum = 0x0004, + all = 0x0005, + local_quorum = 0x0006, + each_quorum = 0x0007, + serial = 0x0008, + local_serial = 0x0009, + local_one = 0x000a } -local types_mt = {} - -function types_mt:__index(key) - if cql_types[key] ~= nil then - return function(value) - return {value = value, type_id = cql_types[key]} - end - end +local ERRORS = { + SERVER = 0x0000, + PROTOCOL = 0x000A, + BAD_CREDENTIALS = 0x0100, + UNAVAILABLE_EXCEPTION = 0x1000, + OVERLOADED = 0x1001, + IS_BOOTSTRAPPING = 0x1002, + TRUNCATE_ERROR = 0x1003, + WRITE_TIMEOUT = 0x1100, + READ_TIMEOUT = 0x1200, + SYNTAX_ERROR = 0x2000, + UNAUTHORIZED = 0x2100, + INVALID = 0x2200, + CONFIG_ERROR = 0x2300, + ALREADY_EXISTS = 0x2400, + UNPREPARED = 0x2500 +} - return rawget(self, key) -end +local ERRORS_TRANSLATION = { + [ERRORS.SERVER] = "Server error", + [ERRORS.PROTOCOL] = "Protocol error", + [ERRORS.BAD_CREDENTIALS] = "Bad credentials", + [ERRORS.UNAVAILABLE_EXCEPTION] = "Unavailable exception", + [ERRORS.OVERLOADED] = "Overloaded", + [ERRORS.IS_BOOTSTRAPPING] = "Is bootstrapping", + [ERRORS.TRUNCATE_ERROR] = "Truncate error", + [ERRORS.WRITE_TIMEOUT] = "Write timeout", + [ERRORS.READ_TIMEOUT] = "Read timeout", + [ERRORS.SYNTAX_ERROR] = "Syntaxe rror", + [ERRORS.UNAUTHORIZED] = "Unauthorized", + [ERRORS.INVALID] = "Invalid", + [ERRORS.CONFIG_ERROR] = "Config error", + [ERRORS.ALREADY_EXISTS] = "Already exists", + [ERRORS.UNPREPARED] = "Unprepared" +} -return setmetatable({ +return { + -- public + consistencies = consistencies, + -- private cql_types = cql_types, - consistencies = consistencies -}, types_mt) + QUERY_FLAGS = QUERY_FLAGS, + OP_CODES = OP_CODES, + ERRORS = ERRORS, + ERRORS_TRANSLATION = ERRORS_TRANSLATION +} From dc1c4d4bc28047ad4e8477b5af21fc7791e2c365 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Wed, 18 Nov 2015 14:51:07 -0800 Subject: [PATCH 30/78] feat(rewrite) avoid reconnection in luasocket mode --- spec/integration/cassandra_spec.lua | 59 +++++++++++++---------- spec/load.lua | 23 +++++++-- spec/unit/cql_types_buffer_spec.lua | 2 +- src/cassandra.lua | 74 ++++++++++++++++++++--------- src/cassandra/errors.lua | 4 +- src/cassandra/types/init.lua | 4 +- 6 files changed, 107 insertions(+), 59 deletions(-) diff --git a/spec/integration/cassandra_spec.lua b/spec/integration/cassandra_spec.lua index d6a5eaf..c64cc39 100644 --- a/spec/integration/cassandra_spec.lua +++ b/spec/integration/cassandra_spec.lua @@ -94,27 +94,22 @@ describe("spawn session", function() }) assert.falsy(err) assert.truthy(session) + assert.truthy(session.hosts) + assert.equal(3, #session.hosts) end) describe(":execute()", function() - describe("ROWS parsing", function() - it("should execute a SELECT query, parsing ROWS", function() - local rows, err = session:execute("SELECT key FROM system.local") - assert.falsy(err) - assert.truthy(rows) - assert.equal("ROWS", rows.type) - assert.equal(1, #rows) - assert.equal("local", rows[1].key) - end) - it("should accept query arguments", function() - local rows, err = session:execute("SELECT key FROM system.local WHERE key = ?", {"local"}) - assert.falsy(err) - assert.truthy(rows) - assert.equal("ROWS", rows.type) - assert.equal(1, #rows) - assert.equal("local", rows[1].key) - end) + teardown(function() + session:execute("DROP KEYSPACE resty_cassandra_spec_parsing") end) - describe("SCHEMA_CHANGE/SET_KEYSPACE parsing", function() + it("should parse ROWS results", function() + local rows, err = session:execute("SELECT key FROM system.local") + assert.falsy(err) + assert.truthy(rows) + assert.equal("ROWS", rows.type) + assert.equal(1, #rows) + assert.equal("local", rows[1].key) + end) + it("should parse SCHEMA_CHANGE/SET_KEYSPACE results", function() local res, err = session:execute [[ CREATE KEYSPACE IF NOT EXISTS resty_cassandra_spec_parsing WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor': 1} @@ -127,14 +122,26 @@ describe("spawn session", function() assert.equal("KEYSPACE", res.keyspace) assert.equal("resty_cassandra_spec_parsing", res.table) + os.execute("sleep 1") + res, err = session:execute [[USE "resty_cassandra_spec_parsing"]] assert.falsy(err) assert.truthy(res) assert.equal(0, #res) assert.equal("SET_KEYSPACE", res.type) assert.equal("resty_cassandra_spec_parsing", res.keyspace) - - res, err = session:execute("DROP KEYSPACE resty_cassandra_spec_parsing") + end) + it("should spawn a session in a given keyspace", function() + local session_in_keyspace, err = cassandra.spawn_session({ + shm = _shm, + keyspace = "resty_cassandra_specs_parsing" + }) + assert.falsy(err) + assert.equal("resty_cassandra_specs_parsing", session_in_keyspace.options.keyspace) + assert.equal("resty_cassandra_specs_parsing", session_in_keyspace.hosts[1].options.keyspace) + end) + it("should parse SCHEMA_CHANGE bis", function() + local res, err = session:execute("DROP KEYSPACE resty_cassandra_spec_parsing") assert.falsy(err) assert.truthy(res) assert.equal(0, #res) @@ -143,7 +150,7 @@ describe("spawn session", function() end) end) -describe("use case", function() +describe("session", function() local session setup(function() @@ -163,9 +170,9 @@ describe("use case", function() local _, err = session:execute [[ CREATE TABLE IF NOT EXISTS resty_cassandra_specs.users( - id uuid PRIMARY KEY, - name varchar, - age int + id uuid PRIMARY KEY, + name varchar, + age int ) ]] assert.falsy(err) @@ -175,7 +182,7 @@ describe("use case", function() local _, err = session:execute("DROP KEYSPACE resty_cassandra_specs") assert.falsy(err) - session:close() + session:shutdown() end) describe(":set_keyspace()", function() @@ -192,7 +199,7 @@ describe("use case", function() describe(":execute()", function() it("should accept values to bind", function() local res, err = session:execute("INSERT INTO users(id, name, age) VALUES(?, ?, ?)", - {cassandra.types.uuid("2644bada-852c-11e3-89fb-e0b9a54a6d93"), "Bob", 42}) + {cassandra.uuid("2644bada-852c-11e3-89fb-e0b9a54a6d93"), "Bob", 42}) assert.falsy(err) assert.truthy(res) assert.equal("VOID", res.type) diff --git a/spec/load.lua b/spec/load.lua index 0b200a5..17a462e 100644 --- a/spec/load.lua +++ b/spec/load.lua @@ -1,9 +1,9 @@ -package.path = "src/?.lua;"..package.path +package.path = "src/?.lua;src/?/init.lua;"..package.path local inspect = require "inspect" local cassandra = require "cassandra" local log = require "cassandra.log" -log.set_lvl("ERR") +log.set_lvl("INFO") local ok, err = cassandra.spawn_cluster { shm = "cassandra", @@ -11,14 +11,20 @@ local ok, err = cassandra.spawn_cluster { } assert(err == nil, inspect(err)) + + + + + local session, err = cassandra.spawn_session { shm = "cassandra" } assert(err == nil, inspect(err)) -local i = 0 -while true do - i = i + 1 +--local i = 0 +--while true do + --i = i + 1 +for i = 1, 1000 do local res, err = session:execute("SELECT peer FROM system.peers") if err then print(inspect(err)) @@ -27,3 +33,10 @@ while true do print("Request "..i.." successful.") end +session:shutdown() + +local res, err = session:execute("SELECT peer FROM system.peers") +if err then + print(inspect(err)) + error() +end diff --git a/spec/unit/cql_types_buffer_spec.lua b/spec/unit/cql_types_buffer_spec.lua index 7bd473a..1127f78 100644 --- a/spec/unit/cql_types_buffer_spec.lua +++ b/spec/unit/cql_types_buffer_spec.lua @@ -37,7 +37,7 @@ describe("CQL Types protocol v"..protocol_version, function() describe("manual type infering", function() it("should be possible to infer the type of a value through helper methods", function() for _, fixture in ipairs(fixture_values) do - local infered_value = cassandra.types[fixture_type](fixture) + local infered_value = cassandra[fixture_type](fixture) local buf = Buffer(protocol_version) buf:write_cql_value(infered_value) buf:reset() diff --git a/src/cassandra.lua b/src/cassandra.lua index db20617..83c3dbe 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -124,14 +124,16 @@ local function startup(self) return self:send(startup_req) end -local function change_keyspace(self) - log.info("Keyspace request. Using keyspace: "..self.options.keyspace) +local function change_keyspace(self, keyspace) + log.info("Keyspace request. Using keyspace: "..keyspace) - local keyspace_req = Requests.KeyspaceRequest(self.options.keyspace) + local keyspace_req = Requests.KeyspaceRequest(keyspace) return self:send(keyspace_req) end function Host:connect() + if self.connected then return true end + log.info("Connecting to "..self.address) self:set_timeout(self.options.socket_options.connect_timeout) @@ -154,7 +156,7 @@ function Host:connect() if err then log.info("Startup request failed. "..err) -- Check for incorrect protocol version - if err and err.code == frame_reader.errors.PROTOCOL then + if err and err.code == CQL_Errors.PROTOCOL then if string_find(err.message, "Invalid or unsupported protocol version:", nil, true) then self:close() self:decrease_version() @@ -172,16 +174,29 @@ function Host:connect() log.info("Host at "..self.address.." is ready with protocol v"..self.protocol_version) if self.options.keyspace ~= nil then - local _, err = change_keyspace(self) + local _, err = change_keyspace(self, self.options.keyspace) if err then return false, err end end + self.connected = true return true end end +function Host:change_keyspace(keyspace) + if self.connected then + self.options.keyspace = keyspace + + local res, err = change_keyspace(self, keyspace) + if err then + log.err("Could not change keyspace for host "..self.address) + end + return res, err + end +end + function Host:set_timeout(t) if self.socket_type == "luasocket" then -- value is in seconds @@ -209,10 +224,11 @@ function Host:set_keep_alive() local ok, err = self.socket:setkeepalive() if err then log.err("Could not set keepalive for socket to "..self.address..". "..err) + return ok, err end - return ok end + self.connected = false return true end @@ -223,6 +239,7 @@ function Host:close() log.err("Could not close socket for connection to "..self.address..". "..err) return false, err else + self.connected = false return true end end @@ -285,8 +302,9 @@ end local RequestHandler = {} -function RequestHandler:new(request, options) +function RequestHandler:new(request, hosts, options) local o = { + hosts = hosts, request = request, options = options, n_retries = 0 @@ -313,13 +331,8 @@ function RequestHandler:get_next_coordinator() local errors = {} local iter = self.options.policies.load_balancing - local hosts, cache_err = cache.get_hosts(self.options.shm) - if cache_err then - return nil, cache_err - end - for _, addr in iter(self.options.shm, hosts) do - local host = Host(addr, self.options) + for _, host in iter(self.options.shm, self.hosts) do local can_host_be_considered_up, cache_err = host:can_be_considered_up() if cache_err then return nil, cache_err @@ -356,8 +369,6 @@ function RequestHandler:send() if coordinator.socket_type == "ngx" then coordinator:set_keep_alive() - else - coordinator:close() end if err then @@ -425,7 +436,6 @@ end --- Session -- A short-lived session, cluster-aware through the cache. --- Uses a load balancing policy to select a coordinator on which to perform requests. -- @section session local Session = {} @@ -435,29 +445,47 @@ function Session:new(options) local s = { options = options, - coordinator = nil -- to be determined by load balancing policy + hosts = {} } + local host_addresses, cache_err = cache.get_hosts(options.shm) + if cache_err then + return nil, cache_err + end + + for _, addr in ipairs(host_addresses) do + table_insert(s.hosts, Host(addr, options)) + end + return setmetatable(s, {__index = self}) end function Session:execute(query, args, options) + if self.terminated then + return nil, Errors.NoHostAvailableError(nil, "Cannot reuse a session that has been shut down.") + end + local q_options = table_utils.deep_copy(self.options) q_options.query_options = table_utils.extend_table(q_options.query_options, options) - local query_request = Requests.QueryRequest(query, args, options) - local request_handler = RequestHandler:new(query_request, q_options) + local query_request = Requests.QueryRequest(query, args, q_options.query_options) + local request_handler = RequestHandler:new(query_request, self.hosts, q_options) return request_handler:send() end function Session:set_keyspace(keyspace) self.options.keyspace = keyspace + for _, host in ipairs(self.hosts) do + host:change_keyspace(keyspace) + end end -function Session:close() - if self.coordinator ~= nil then - return self.coordinator:close() +function Session:shutdown() + for _, host in ipairs(self.hosts) do + host:close() end + self.hosts = {} + self.terminated = true end --- Cassandra @@ -567,7 +595,7 @@ function types_mt:__index(key) return rawget(self, key) end -Cassandra.types = setmetatable({}, types_mt) +setmetatable(Cassandra, types_mt) Cassandra.consistencies = types.consistencies diff --git a/src/cassandra/errors.lua b/src/cassandra/errors.lua index 459b074..398c477 100644 --- a/src/cassandra/errors.lua +++ b/src/cassandra/errors.lua @@ -8,9 +8,9 @@ local string_format = string.format local ERROR_TYPES = { NoHostAvailableError = { info = "Represents an error when a query cannot be performed because no host is available or could be reached by the driver.", - message = function(errors) + message = function(errors, msg) if type(errors) ~= "table" then - error("NoHostAvailableError must be given a list of errors") + return msg end local message = "All hosts tried for query failed." diff --git a/src/cassandra/types/init.lua b/src/cassandra/types/init.lua index 51061d8..b79ded7 100644 --- a/src/cassandra/types/init.lua +++ b/src/cassandra/types/init.lua @@ -79,7 +79,7 @@ local ERRORS = { UNPREPARED = 0x2500 } -local ERRORS_TRANSLATION = { +local ERRORS_TRANSLATIONS = { [ERRORS.SERVER] = "Server error", [ERRORS.PROTOCOL] = "Protocol error", [ERRORS.BAD_CREDENTIALS] = "Bad credentials", @@ -105,5 +105,5 @@ return { QUERY_FLAGS = QUERY_FLAGS, OP_CODES = OP_CODES, ERRORS = ERRORS, - ERRORS_TRANSLATION = ERRORS_TRANSLATION + ERRORS_TRANSLATIONS = ERRORS_TRANSLATIONS } From ed1350f707aec4b04b36425e96811bc482c9c6d4 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Wed, 18 Nov 2015 15:30:12 -0800 Subject: [PATCH 31/78] fix(tests) better tests for keyspace switching --- spec/integration/cassandra_spec.lua | 53 ++++++++++++++++++++++++----- src/cassandra.lua | 33 ++++++++++++------ 2 files changed, 67 insertions(+), 19 deletions(-) diff --git a/spec/integration/cassandra_spec.lua b/spec/integration/cassandra_spec.lua index c64cc39..be8d57b 100644 --- a/spec/integration/cassandra_spec.lua +++ b/spec/integration/cassandra_spec.lua @@ -6,6 +6,11 @@ local cassandra = require "cassandra" local log = require "cassandra.log" +local function sleep(t) + if not t then t = 1 end + os.execute("sleep "..t) +end + -- Define log level for tests log.set_lvl("ERR") @@ -99,6 +104,7 @@ describe("spawn session", function() end) describe(":execute()", function() teardown(function() + -- drop keyspace in case tests failed session:execute("DROP KEYSPACE resty_cassandra_spec_parsing") end) it("should parse ROWS results", function() @@ -122,7 +128,7 @@ describe("spawn session", function() assert.equal("KEYSPACE", res.keyspace) assert.equal("resty_cassandra_spec_parsing", res.table) - os.execute("sleep 1") + sleep() res, err = session:execute [[USE "resty_cassandra_spec_parsing"]] assert.falsy(err) @@ -134,11 +140,27 @@ describe("spawn session", function() it("should spawn a session in a given keyspace", function() local session_in_keyspace, err = cassandra.spawn_session({ shm = _shm, - keyspace = "resty_cassandra_specs_parsing" + keyspace = "resty_cassandra_spec_parsing" }) assert.falsy(err) - assert.equal("resty_cassandra_specs_parsing", session_in_keyspace.options.keyspace) - assert.equal("resty_cassandra_specs_parsing", session_in_keyspace.hosts[1].options.keyspace) + assert.equal("resty_cassandra_spec_parsing", session_in_keyspace.options.keyspace) + assert.equal("resty_cassandra_spec_parsing", session_in_keyspace.hosts[1].options.keyspace) + + local _, err = session:execute [[ + CREATE TABLE IF NOT EXISTS resty_cassandra_spec_parsing.users( + id uuid PRIMARY KEY, + name varchar, + age int + ) + ]] + assert.falsy(err) + + sleep() + + local rows, err = session_in_keyspace:execute("SELECT * FROM users") + assert.falsy(err) + assert.truthy(rows) + assert.equal(0, #rows) end) it("should parse SCHEMA_CHANGE bis", function() local res, err = session:execute("DROP KEYSPACE resty_cassandra_spec_parsing") @@ -155,9 +177,7 @@ describe("session", function() setup(function() local err - session, err = cassandra.spawn_session { - shm = _shm - } + session, err = cassandra.spawn_session {shm = _shm} assert.falsy(err) local _, err = session:execute [[ @@ -166,7 +186,7 @@ describe("session", function() ]] assert.falsy(err) - os.execute("sleep 1") + sleep() local _, err = session:execute [[ CREATE TABLE IF NOT EXISTS resty_cassandra_specs.users( @@ -176,9 +196,16 @@ describe("session", function() ) ]] assert.falsy(err) + + sleep() end) teardown(function() + -- drop keyspace in case tests failed + local err + session, err = cassandra.spawn_session {shm = _shm} + assert.falsy(err) + local _, err = session:execute("DROP KEYSPACE resty_cassandra_specs") assert.falsy(err) @@ -187,7 +214,9 @@ describe("session", function() describe(":set_keyspace()", function() it("should set a session's 'keyspace' option", function() - session:set_keyspace("resty_cassandra_specs") + local ok, err = session:set_keyspace("resty_cassandra_specs") + assert.falsy(err) + assert.True(ok) assert.equal("resty_cassandra_specs", session.options.keyspace) local rows, err = session:execute("SELECT * FROM users") @@ -205,4 +234,10 @@ describe("session", function() assert.equal("VOID", res.type) end) end) + + describe(":shutdown()", function() + session:shutdown() + assert.True(session.terminated) + assert.same({}, session.hosts) + end) end) diff --git a/src/cassandra.lua b/src/cassandra.lua index 83c3dbe..6686fae 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -141,7 +141,7 @@ function Host:connect() local ok, err = self.socket:connect(self.host, self.port) if ok ~= 1 then log.info("Could not connect to "..self.address..". Reason: "..err) - return false, err + return false, err, true end log.info("Session connected to "..self.address) @@ -176,6 +176,7 @@ function Host:connect() if self.options.keyspace ~= nil then local _, err = change_keyspace(self, self.options.keyspace) if err then + log.err("Could not set keyspace. "..err) return false, err end end @@ -329,7 +330,6 @@ end function RequestHandler:get_next_coordinator() local errors = {} - local iter = self.options.policies.load_balancing for _, host in iter(self.options.shm, self.hosts) do @@ -337,20 +337,23 @@ function RequestHandler:get_next_coordinator() if cache_err then return nil, cache_err elseif can_host_be_considered_up then - local connected, err = host:connect() + local connected, err, maybe_down = host:connect() if connected then self.coordinator = host return host else - -- bad host, setting DOWN - local ok, cache_err = host:set_down() - if not ok then - return nil, cache_err + if maybe_down then + -- only on socket connect error + -- might be a bad host, setting DOWN + local ok, cache_err = host:set_down() + if not ok then + return nil, cache_err + end end - errors[addr] = err + errors[host.address] = err end else - errors[addr] = "Host considered DOWN" + errors[host.address] = "Host considered DOWN" end end @@ -474,10 +477,20 @@ function Session:execute(query, args, options) end function Session:set_keyspace(keyspace) + local errors = {} self.options.keyspace = keyspace for _, host in ipairs(self.hosts) do - host:change_keyspace(keyspace) + local _, err = host:change_keyspace(keyspace) + if err then + table_insert(errors, err) + end end + + if #errors > 0 then + return false, errors + end + + return true end function Session:shutdown() From 754dc54676c9f75854e198a399a79ddc9ec7dd1c Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Wed, 18 Nov 2015 16:18:35 -0800 Subject: [PATCH 32/78] feat(rewrite) bigint type and options tests --- spec/integration/cassandra_spec.lua | 28 ++++++++++++ spec/unit/cql_types_buffer_spec.lua | 1 + spec/unit/options_spec.lua | 68 +++++++++++++++++++++++++++++ src/cassandra/buffer.lua | 5 ++- src/cassandra/options.lua | 9 ++-- src/cassandra/types/bigint.lua | 26 +++++++++++ 6 files changed, 133 insertions(+), 4 deletions(-) create mode 100644 spec/unit/options_spec.lua create mode 100644 src/cassandra/types/bigint.lua diff --git a/spec/integration/cassandra_spec.lua b/spec/integration/cassandra_spec.lua index be8d57b..1780f49 100644 --- a/spec/integration/cassandra_spec.lua +++ b/spec/integration/cassandra_spec.lua @@ -81,6 +81,16 @@ describe("spawn cluster", function() assert.False(ok) assert.equal("NoHostAvailableError", err.type) end) + it("should accept a custom port through an option", function() + local ok, err = cassandra.spawn_cluster({ + shm = "test", + protocol_options = {default_port = 9043}, + contact_points = _contact_points + }) + assert.truthy(err) + assert.False(ok) + assert.equal("NoHostAvailableError", err.type) + end) end) describe("spawn session", function() @@ -232,6 +242,24 @@ describe("session", function() assert.falsy(err) assert.truthy(res) assert.equal("VOID", res.type) + + local rows, err = session:execute("SELECT * FROM users WHERE id = 2644bada-852c-11e3-89fb-e0b9a54a6d93") + assert.falsy(err) + assert.truthy(rows) + assert.equal(1, #rows) + assert.equal("Bob", rows[1].name) + end) + it("support somewhat heavier insertions", function() + for i = 1, 1000 do + local res, err = session:execute("INSERT INTO users(id, name, age) VALUES(uuid(), ?, ?)", {"Alice", 33}) + assert.falsy(err) + assert.truthy(res) + end + + local rows, err = session:execute("SELECT COUNT(*) FROM users") + assert.falsy(err) + assert.truthy(rows) + assert.equal(1001, rows[1].count) end) end) diff --git a/spec/unit/cql_types_buffer_spec.lua b/spec/unit/cql_types_buffer_spec.lua index 1127f78..aaec949 100644 --- a/spec/unit/cql_types_buffer_spec.lua +++ b/spec/unit/cql_types_buffer_spec.lua @@ -8,6 +8,7 @@ for _, protocol_version in ipairs(CONSTS.SUPPORTED_PROTOCOL_VERSIONS) do describe("CQL Types protocol v"..protocol_version, function() local FIXTURES = { + bigint = {0, 42, -42, 42000000000, -42000000000}, boolean = {true, false}, inet = { "127.0.0.1", "0.0.0.1", "8.8.8.8", diff --git a/spec/unit/options_spec.lua b/spec/unit/options_spec.lua new file mode 100644 index 0000000..40eb713 --- /dev/null +++ b/spec/unit/options_spec.lua @@ -0,0 +1,68 @@ +local cassandra = require "cassandra" + +describe("options parsing", function() + describe("spawn_cluster", function() + it("should require shm", function() + assert.has_error(function() + cassandra.spawn_cluster() + end, "shm is required for spawning a cluster/session") + + assert.has_error(function() + cassandra.spawn_cluster({shm = 123}) + end, "shm must be a string") + + assert.has_error(function() + cassandra.spawn_cluster({shm = ""}) + end, "shm must be a valid string") + end) + it("should require contact_points", function() + assert.has_error(function() + cassandra.spawn_cluster({ + shm = "test" + }) + end, "contact_points must contain at least one contact point") + + assert.has_error(function() + cassandra.spawn_cluster({ + shm = "test", + contact_points = {foo = "bar"} + }) + end, "contact_points must be an array (integer-indexed table)") + end) + end) + describe("spawn_session", function() + it("should require shm", function() + assert.has_error(function() + cassandra.spawn_session() + end, "shm is required for spawning a cluster/session") + + assert.has_error(function() + cassandra.spawn_session({shm = 123}) + end, "shm must be a string") + + assert.has_error(function() + cassandra.spawn_session({shm = ""}) + end, "shm must be a valid string") + end) + it("should validate protocol_options", function() + assert.has_error(function() + cassandra.spawn_session({ + shm = "test", + protocol_options = { + default_port = "" + } + }) + end, "protocol default_port must be a number") + end) + it("should validate policies", function() + assert.has_error(function() + cassandra.spawn_session({ + shm = "test", + policies = { + address_resolution = "" + } + }) + end, "address_resolution policy must be a function") + end) + end) +end) diff --git a/src/cassandra/buffer.lua b/src/cassandra/buffer.lua index dc01ef4..058d3d3 100644 --- a/src/cassandra/buffer.lua +++ b/src/cassandra/buffer.lua @@ -44,7 +44,7 @@ end local CQL_DECODERS = { -- custom = 0x00, [cql_types.ascii] = "raw", - -- [cql_types.bigint] = "bigint", + [cql_types.bigint] = "bigint", [cql_types.blob] = "raw", [cql_types.boolean] = "boolean", -- [cql_types.counter] = "counter", @@ -115,6 +115,9 @@ function Buffer:write_cql_value(...) end function Buffer:read_cql_value(assumed_type) + if CQL_DECODERS[assumed_type.type_id] == nil then + error("ATTEMPT TO USE NON IMPLEMENTED DECODER FOR TYPE ID: "..assumed_type.type_id) + end local decoder = "read_cql_"..CQL_DECODERS[assumed_type.type_id] return Buffer[decoder](self, assumed_type.value_type_id) end diff --git a/src/cassandra/options.lua b/src/cassandra/options.lua index e2de0a1..444b2ec 100644 --- a/src/cassandra/options.lua +++ b/src/cassandra/options.lua @@ -32,11 +32,12 @@ local DEFAULTS = { } local function parse_session(options) + if options == nil then options = {} end utils.extend_table(DEFAULTS, options) - --if type(options.keyspace) ~= "string" then - --error("keyspace must be a string") - --end + if options.keyspace ~= nil then + assert(type(options.keyspace) == "string", "keyspace must be a string") + end assert(options.shm ~= nil, "shm is required for spawning a cluster/session") assert(type(options.shm) == "string", "shm must be a string") @@ -63,6 +64,8 @@ local function parse_cluster(options) error("contact_points must contain at least one contact point") end + options.keyspace = nil -- it makes no sense to use keyspace in this context + return options end diff --git a/src/cassandra/types/bigint.lua b/src/cassandra/types/bigint.lua new file mode 100644 index 0000000..b8402ca --- /dev/null +++ b/src/cassandra/types/bigint.lua @@ -0,0 +1,26 @@ +local string_char = string.char +local math_floor = math.floor +local string_byte = string.byte + +return { + repr = function(self, val) + local first_byte = val >= 0 and 0 or 0xFF + return string_char(first_byte, -- only 53 bits from double + math_floor(val / 0x1000000000000) % 0x100, + math_floor(val / 0x10000000000) % 0x100, + math_floor(val / 0x100000000) % 0x100, + math_floor(val / 0x1000000) % 0x100, + math_floor(val / 0x10000) % 0x100, + math_floor(val / 0x100) % 0x100, + val % 0x100) + end, + read = function(buffer) + local bytes = buffer:read(8) + local b1, b2, b3, b4, b5, b6, b7, b8 = string_byte(bytes, 1, 8) + if b1 < 0x80 then + return ((((((b1 * 0x100 + b2) * 0x100 + b3) * 0x100 + b4) * 0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8 + else + return ((((((((b1 - 0xFF) * 0x100 + (b2 - 0xFF)) * 0x100 + (b3 - 0xFF)) * 0x100 + (b4 - 0xFF)) * 0x100 + (b5 - 0xFF)) * 0x100 + (b6 - 0xFF)) * 0x100 + (b7 - 0xFF)) * 0x100 + (b8 - 0xFF)) - 1 + end + end +} From 581547c78ced33031fe2369c802dfdebbe0d9f7b Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Wed, 18 Nov 2015 20:59:37 -0800 Subject: [PATCH 33/78] feat(rewrite) more CQL types --- spec/integration/cassandra_spec.lua | 43 ++++------ spec/integration/cql_types_spec.lua | 119 +++++++++++++++++++++++++++ spec/spec_utils.lua | 123 ++++++++++++++++++++++++++++ spec/unit/buffer_spec.lua | 1 + spec/unit/cql_types_buffer_spec.lua | 61 ++++++-------- src/cassandra/buffer.lua | 26 ++++-- src/cassandra/types/double.lua | 63 ++++++++++++++ src/cassandra/types/float.lua | 63 ++++++++++++++ src/cassandra/types/options.lua | 2 +- 9 files changed, 431 insertions(+), 70 deletions(-) create mode 100644 spec/integration/cql_types_spec.lua create mode 100644 spec/spec_utils.lua create mode 100644 src/cassandra/types/double.lua create mode 100644 src/cassandra/types/float.lua diff --git a/spec/integration/cassandra_spec.lua b/spec/integration/cassandra_spec.lua index 1780f49..33e5e71 100644 --- a/spec/integration/cassandra_spec.lua +++ b/spec/integration/cassandra_spec.lua @@ -3,33 +3,27 @@ -- with fallback on LuaSocket when it is the case. Those integration tests must -- mimic the ones running in ngx_lua. +local utils = require "spec.spec_utils" local cassandra = require "cassandra" -local log = require "cassandra.log" - -local function sleep(t) - if not t then t = 1 end - os.execute("sleep "..t) -end -- Define log level for tests -log.set_lvl("ERR") +utils.set_log_lvl("ERR") local _shm = "cassandra_specs" -local _contact_points = {"127.0.0.1", "127.0.0.2"} describe("spawn cluster", function() it("should require a 'shm' option", function() assert.has_error(function() cassandra.spawn_cluster({ shm = nil, - contact_points = _contact_points + contact_points = utils.contact_points }) end, "shm is required for spawning a cluster/session") end) it("should spawn a cluster", function() local ok, err = cassandra.spawn_cluster({ shm = _shm, - contact_points = _contact_points + contact_points = utils.contact_points }) assert.falsy(err) assert.True(ok) @@ -48,7 +42,7 @@ describe("spawn cluster", function() end) it("should iterate over contact_points to find an entrance into the cluster", function() local contact_points = {"0.0.0.1", "0.0.0.2", "0.0.0.3"} - contact_points[#contact_points + 1] = _contact_points[1] + contact_points[#contact_points + 1] = utils.contact_points[1] local ok, err = cassandra.spawn_cluster({ shm = "test", @@ -70,7 +64,7 @@ describe("spawn cluster", function() end) it("should accept a custom port for given hosts", function() local contact_points = {} - for i, addr in ipairs(_contact_points) do + for i, addr in ipairs(utils.contact_points) do contact_points[i] = addr..":9043" end local ok, err = cassandra.spawn_cluster({ @@ -85,7 +79,7 @@ describe("spawn cluster", function() local ok, err = cassandra.spawn_cluster({ shm = "test", protocol_options = {default_port = 9043}, - contact_points = _contact_points + contact_points = utils.contact_points }) assert.truthy(err) assert.False(ok) @@ -138,7 +132,7 @@ describe("spawn session", function() assert.equal("KEYSPACE", res.keyspace) assert.equal("resty_cassandra_spec_parsing", res.table) - sleep() + utils.wait() res, err = session:execute [[USE "resty_cassandra_spec_parsing"]] assert.falsy(err) @@ -165,7 +159,7 @@ describe("spawn session", function() ]] assert.falsy(err) - sleep() + utils.wait() local rows, err = session_in_keyspace:execute("SELECT * FROM users") assert.falsy(err) @@ -184,19 +178,14 @@ end) describe("session", function() local session + local _KEYSPACE = "resty_cassandra_specs" setup(function() local err session, err = cassandra.spawn_session {shm = _shm} assert.falsy(err) - local _, err = session:execute [[ - CREATE KEYSPACE IF NOT EXISTS resty_cassandra_specs - WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor': 1} - ]] - assert.falsy(err) - - sleep() + utils.create_keyspace(session, _KEYSPACE) local _, err = session:execute [[ CREATE TABLE IF NOT EXISTS resty_cassandra_specs.users( @@ -207,7 +196,7 @@ describe("session", function() ]] assert.falsy(err) - sleep() + utils.wait() end) teardown(function() @@ -216,18 +205,16 @@ describe("session", function() session, err = cassandra.spawn_session {shm = _shm} assert.falsy(err) - local _, err = session:execute("DROP KEYSPACE resty_cassandra_specs") - assert.falsy(err) - + utils.drop_keyspace(session, _KEYSPACE) session:shutdown() end) describe(":set_keyspace()", function() it("should set a session's 'keyspace' option", function() - local ok, err = session:set_keyspace("resty_cassandra_specs") + local ok, err = session:set_keyspace(_KEYSPACE) assert.falsy(err) assert.True(ok) - assert.equal("resty_cassandra_specs", session.options.keyspace) + assert.equal(_KEYSPACE, session.options.keyspace) local rows, err = session:execute("SELECT * FROM users") assert.falsy(err) diff --git a/spec/integration/cql_types_spec.lua b/spec/integration/cql_types_spec.lua new file mode 100644 index 0000000..dbc78a3 --- /dev/null +++ b/spec/integration/cql_types_spec.lua @@ -0,0 +1,119 @@ +local utils = require "spec.spec_utils" +local cassandra = require "cassandra" + +local _shm = "cql_types" +local _KEYSPACE = "resty_cassandra_cql_types_specs" + +-- Define log level for tests +utils.set_log_lvl("ERR") + +describe("CQL types integration", function() + local session + + setup(function() + local _, err = cassandra.spawn_cluster({ + shm = _shm, + contact_points = utils.contact_points + }) + assert.falsy(err) + + session, err = cassandra.spawn_session({shm = _shm}) + assert.falsy(err) + + utils.create_keyspace(session, _KEYSPACE) + + _, err = session:set_keyspace(_KEYSPACE) + assert.falsy(err) + + _, err = session:execute [[ + CREATE TABLE IF NOT EXISTS all_types( + id uuid PRIMARY KEY, + ascii_sample ascii, + bigint_sample bigint, + blob_sample blob, + boolean_sample boolean, + decimal_sample decimal, + double_sample double, + float_sample float, + int_sample int, + text_sample text, + timestamp_sample timestamp, + varchar_sample varchar, + varint_sample varint, + timeuuid_sample timeuuid, + inet_sample inet, + list_sample_text list, + list_sample_int list, + map_sample map, + set_sample_text set, + set_sample_int set + ) + ]] + assert.falsy(err) + + utils.wait() + end) + + teardown(function() + utils.drop_keyspace(session, _KEYSPACE) + session:shutdown() + end) + + local _UUID = "1144bada-852c-11e3-89fb-e0b9a54a6d11" + + for fixture_type, fixture_values in pairs(utils.cql_fixtures) do + it("["..fixture_type.."] should be inserted and retrieved", function() + local insert_query = string.format("INSERT INTO all_types(id, %s_sample) VALUES(?, ?)", fixture_type) + local select_query = string.format("SELECT %s_sample FROM all_types WHERE id = ?", fixture_type) + + for _, fixture in ipairs(fixture_values) do + local res, err = session:execute(insert_query, {cassandra.uuid(_UUID), cassandra[fixture_type](fixture)}) + assert.falsy(err) + assert.truthy(res) + + local rows, err = session:execute(select_query, {cassandra.uuid(_UUID)}) + assert.falsy(err) + assert.truthy(rows) + + local decoded = rows[1][fixture_type.."_sample"] + assert.validFixture(fixture_type, fixture, decoded) + end + end) + end + + it("[list] should be inserted and retrieved", function() + for _, fixture in ipairs(utils.cql_list_fixtures) do + local insert_query = string.format("INSERT INTO all_types(id, list_sample_%s) VALUES(?, ?)", fixture.type_name) + local select_query = string.format("SELECT list_sample_%s FROM all_types WHERE id = ?", fixture.type_name) + + local res, err = session:execute(insert_query, {cassandra.uuid(_UUID), cassandra.list(fixture.value)}) + assert.falsy(err) + assert.truthy(res) + + local rows, err = session:execute(select_query, {cassandra.uuid(_UUID)}) + assert.falsy(err) + assert.truthy(rows) + + local decoded = rows[1]["list_sample_"..fixture.type_name] + assert.validFixture("list", fixture.value, decoded) + end + end) + + it("[set] should be inserted and retrieved", function() + for _, fixture in ipairs(utils.cql_list_fixtures) do + local insert_query = string.format("INSERT INTO all_types(id, set_sample_%s) VALUES(?, ?)", fixture.type_name) + local select_query = string.format("SELECT set_sample_%s FROM all_types WHERE id = ?", fixture.type_name) + + local res, err = session:execute(insert_query, {cassandra.uuid(_UUID), cassandra.set(fixture.value)}) + assert.falsy(err) + assert.truthy(res) + + local rows, err = session:execute(select_query, {cassandra.uuid(_UUID)}) + assert.falsy(err) + assert.truthy(rows) + + local decoded = rows[1]["set_sample_"..fixture.type_name] + assert.sameSet(fixture.value, decoded) + end + end) +end) diff --git a/spec/spec_utils.lua b/spec/spec_utils.lua new file mode 100644 index 0000000..1a54774 --- /dev/null +++ b/spec/spec_utils.lua @@ -0,0 +1,123 @@ +local say = require "say" +local log = require "cassandra.log" +local types = require "cassandra.types" +local assert = require "luassert.assert" + +local _M = {} + +function _M.wait(t) + if not t then t = 1 end + os.execute("sleep "..t) +end + +function _M.set_log_lvl(lvl) + log.set_lvl(lvl) +end + +function _M.create_keyspace(session, keyspace) + local res, err = session:execute([[ + CREATE KEYSPACE IF NOT EXISTS ]]..keyspace..[[ + WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor': 1} + ]]) + if err then + error(err) + end + + _M.wait() + + return res +end + +function _M.drop_keyspace(session, keyspace) + local res, err = session:execute("DROP KEYSPACE "..keyspace) + if err then + error(err) + end + return res +end + +local delta = 0.0000001 +local function validFixture(state, arguments) + local fixture_type, fixture, decoded = unpack(arguments) + + local result + if fixture_type == "float" then + result = math.abs(decoded - fixture) < delta + elseif type(fixture) == "table" then + result = pcall(assert.same, fixture, decoded) + else + result = pcall(assert.equal, fixture, decoded) + end + return result +end + +local function sameSet(state, arguments) + local fixture, decoded = unpack(arguments) + + for _, x in ipairs(fixture) do + local has = false + for _, y in ipairs(decoded) do + if y == x then + has = true + break + end + end + if not has then + return false + end + end + + return true +end + +say:set("assertion.sameSet.positive", "Fixture and decoded value do not match") +say:set("assertion.sameSet.negative", "Fixture and decoded value do not match") +assert:register("assertion", "sameSet", sameSet, "assertion.sameSet.positive", "assertion.sameSet.negative") + +say:set("assertion.validFixture.positive", "Fixture and decoded value do not match") +say:set("assertion.validFixture.negative", "Fixture and decoded value do not match") +assert:register("assertion", "validFixture", validFixture, "assertion.validFixture.positive", "assertion.validFixture.negative") + +_M.cql_fixtures = { + -- custom + ascii = {"Hello world", ""}, + bigint = {0, 42, -42, 42000000000, -42000000000}, + boolean = {true, false}, + -- counter + double = {0, 1.0000000000000004, -1.0000000000000004}, + float = {0, 3.14151, -3.14151}, + inet = { + "127.0.0.1", "0.0.0.1", "8.8.8.8", + "2001:0db8:85a3:0042:1000:8a2e:0370:7334", + "2001:0db8:0000:0000:0000:0000:0000:0001" + }, + int = {0, 4200, -42}, + text = {"Hello world", ""}, + -- list + -- map + -- set + -- uuid + timestamp = {1405356926}, + varchar = {"Hello world", ""}, + varint = {0, 4200, -42}, + timeuuid = {"1144bada-852c-11e3-89fb-e0b9a54a6d11"} + -- udt + -- tuple +} + +_M.cql_list_fixtures = { + {value_type = types.cql_types.text, type_name = "text", value = {"abc", "def"}}, + {value_type = types.cql_types.int, type_name = "int", value = {1, 2 , 0, -42, 42}} +} + +_M.cql_map_fixtures = { + {key_type = types.cql_types.text, value_type = types.cql_types.text, value = {k1 = "v1", k2 = "v2"}}, + {key_type = types.cql_types.text, value_type = types.cql_types.int, value = {k1 = 1, k2 = 2}}, + {key_type = types.cql_types.text, value_type = types.cql_types.int, value = {}} +} + +_M.cql_set_fixtures = _M.cql_list_fixtures + +_M.contact_points = {"127.0.0.1", "127.0.0.2"} + +return _M diff --git a/spec/unit/buffer_spec.lua b/spec/unit/buffer_spec.lua index c3f19a8..c9efa11 100644 --- a/spec/unit/buffer_spec.lua +++ b/spec/unit/buffer_spec.lua @@ -1,6 +1,7 @@ local Buffer = require "cassandra.buffer" describe("Buffer", function() + -- protocol types (different than CQL types) local FIXTURES = { byte = {1, 2, 3}, int = {0, 4200, -42}, diff --git a/spec/unit/cql_types_buffer_spec.lua b/spec/unit/cql_types_buffer_spec.lua index aaec949..97af681 100644 --- a/spec/unit/cql_types_buffer_spec.lua +++ b/spec/unit/cql_types_buffer_spec.lua @@ -1,3 +1,4 @@ +local utils = require "spec.spec_utils" local cassandra = require "cassandra" local Buffer = require "cassandra.buffer" local CONSTS = require "cassandra.constants" @@ -7,19 +8,16 @@ local CQL_TYPES = types.cql_types for _, protocol_version in ipairs(CONSTS.SUPPORTED_PROTOCOL_VERSIONS) do describe("CQL Types protocol v"..protocol_version, function() - local FIXTURES = { - bigint = {0, 42, -42, 42000000000, -42000000000}, - boolean = {true, false}, - inet = { - "127.0.0.1", "0.0.0.1", "8.8.8.8", - "2001:0db8:85a3:0042:1000:8a2e:0370:7334", - "2001:0db8:0000:0000:0000:0000:0000:0001" - }, - int = {0, 4200, -42}, - uuid = {"1144bada-852c-11e3-89fb-e0b9a54a6d11"} - } + it("[uuid] should be bufferable", function() + local fixture = "1144bada-852c-11e3-89fb-e0b9a54a6d11" + local buf = Buffer(protocol_version) + buf:write_cql_uuid(fixture) + buf:reset() + local decoded = buf:read_cql_uuid() + assert.equal(fixture, decoded) + end) - for fixture_type, fixture_values in pairs(FIXTURES) do + for fixture_type, fixture_values in pairs(utils.cql_fixtures) do it("["..fixture_type.."] should be bufferable", function() for _, fixture in ipairs(fixture_values) do local buf = Buffer(protocol_version) @@ -27,16 +25,12 @@ describe("CQL Types protocol v"..protocol_version, function() buf:reset() local decoded = buf["read_cql_"..fixture_type](buf) - if type(fixture) == "table" then - assert.same(fixture, decoded) - else - assert.equal(fixture, decoded) - end + assert.validFixture(fixture_type, fixture, decoded) end end) describe("manual type infering", function() - it("should be possible to infer the type of a value through helper methods", function() + it("["..fixture_type.."] should be possible to infer the type of a value through short-hand methods", function() for _, fixture in ipairs(fixture_values) do local infered_value = cassandra[fixture_type](fixture) local buf = Buffer(protocol_version) @@ -44,24 +38,24 @@ describe("CQL Types protocol v"..protocol_version, function() buf:reset() local decoded = buf:read_cql_value({type_id = CQL_TYPES[fixture_type]}) - if type(fixture) == "table" then - assert.same(fixture, decoded) - else - assert.equal(fixture, decoded) - end + assert.validFixture(fixture_type, fixture, decoded) end end) end) end - it("[map] should be bufferable", function() - local MAP_FIXTURES = { - {key_type = CQL_TYPES.text, value_type = CQL_TYPES.text, value = {k1 = "v1", k2 = "v2"}}, - {key_type = CQL_TYPES.text, value_type = CQL_TYPES.int, value = {k1 = 1, k2 = 2}}, - {key_type = CQL_TYPES.text, value_type = CQL_TYPES.int, value = {}} - } + it("[list] should be bufferable", function() + for _, fixture in ipairs(utils.cql_list_fixtures) do + local buf = Buffer(protocol_version) + buf:write_cql_set(fixture.value) + buf:reset() + local decoded = buf:read_cql_list({type_id = fixture.value_type}) + assert.same(fixture.value, decoded) + end + end) - for _, fixture in ipairs(MAP_FIXTURES) do + it("[map] should be bufferable", function() + for _, fixture in ipairs(utils.cql_map_fixtures) do local buf = Buffer(protocol_version) buf:write_cql_map(fixture.value) buf:reset() @@ -71,12 +65,7 @@ describe("CQL Types protocol v"..protocol_version, function() end) it("[set] should be bufferable", function() - local SET_FIXTURES = { - {value_type = CQL_TYPES.text, value = {"abc", "def"}}, - {value_type = CQL_TYPES.int, value = {1, 2 , 0, -42, 42}} - } - - for _, fixture in ipairs(SET_FIXTURES) do + for _, fixture in ipairs(utils.cql_set_fixtures) do local buf = Buffer(protocol_version) buf:write_cql_set(fixture.value) buf:reset() diff --git a/src/cassandra/buffer.lua b/src/cassandra/buffer.lua index 058d3d3..414fe76 100644 --- a/src/cassandra/buffer.lua +++ b/src/cassandra/buffer.lua @@ -49,8 +49,8 @@ local CQL_DECODERS = { [cql_types.boolean] = "boolean", -- [cql_types.counter] = "counter", -- decimal 0x06 - -- [cql_types.double] = "double", - -- [cql_types.float] = "float", + [cql_types.double] = "double", + [cql_types.float] = "float", [cql_types.inet] = "inet", [cql_types.int] = "int", [cql_types.text] = "raw", @@ -58,14 +58,22 @@ local CQL_DECODERS = { [cql_types.map] = "map", [cql_types.set] = "set", [cql_types.uuid] = "uuid", - -- [cql_types.timestamp] = "timestamp", + [cql_types.timestamp] = "bigint", [cql_types.varchar] = "raw", - -- [cql_types.varint] = "varint", - -- [cql_types.timeuuid] = "timeuuid", + [cql_types.varint] = "int", + [cql_types.timeuuid] = "uuid", -- [cql_types.udt] = "udt", -- [cql_types.tuple] = "tuple" } +local ALIASES = { + raw = {"ascii", "blob", "text", "varchar"}, + bigint = {"timestamp"}, + int = {"varint"}, + set = {"list"}, + uuid = {"timeuuid"} +} + for _, cql_decoder in pairs(CQL_DECODERS) do local mod = require("cassandra.types."..cql_decoder) Buffer["repr_cql_"..cql_decoder] = function(self, ...) @@ -81,6 +89,14 @@ for _, cql_decoder in pairs(CQL_DECODERS) do local buf = Buffer(self.version, bytes) return mod.read(buf, ...) end + + if ALIASES[cql_decoder] ~= nil then + for _, alias in ipairs(ALIASES[cql_decoder]) do + Buffer["repr_cql_"..alias] = Buffer["repr_cql_"..cql_decoder] + Buffer["write_cql_"..alias] = Buffer["write_cql_"..cql_decoder] + Buffer["read_cql_"..alias] = Buffer["read_cql_"..cql_decoder] + end + end end function Buffer:repr_cql_value(value) diff --git a/src/cassandra/types/double.lua b/src/cassandra/types/double.lua new file mode 100644 index 0000000..90604ef --- /dev/null +++ b/src/cassandra/types/double.lua @@ -0,0 +1,63 @@ +local string_char = string.char +local string_byte = string.byte +local math_ldexp = math.ldexp +local math_frexp = math.frexp +local math_floor = math.floor + +return { + repr = function(self, number) + local sign = 0 + if number < 0.0 then + sign = 0x80 + number = -number + end + local mantissa, exponent = math_frexp(number) + if mantissa ~= mantissa then + return string_char(0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) -- nan + elseif mantissa == math.huge then + if sign == 0 then + return string_char(0x7F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) -- +inf + else + return string_char(0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) -- -inf + end + elseif mantissa == 0.0 and exponent == 0 then + return string_char(sign, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) -- zero + else + exponent = exponent + 0x3FE + mantissa = (mantissa * 2.0 - 1.0) * math_ldexp(0.5, 53) + return string_char(sign + math_floor(exponent / 0x10), + (exponent % 0x10) * 0x10 + math_floor(mantissa / 0x1000000000000), + math_floor(mantissa / 0x10000000000) % 0x100, + math_floor(mantissa / 0x100000000) % 0x100, + math_floor(mantissa / 0x1000000) % 0x100, + math_floor(mantissa / 0x10000) % 0x100, + math_floor(mantissa / 0x100) % 0x100, + mantissa % 0x100) + end + end, + read = function(buffer) + local bytes = buffer:read(8) + local b1, b2, b3, b4, b5, b6, b7, b8 = string_byte(bytes, 1, 8) + local sign = b1 > 0x7F + local exponent = (b1 % 0x80) * 0x10 + math_floor(b2 / 0x10) + local mantissa = ((((((b2 % 0x10) * 0x100 + b3) * 0x100 + b4) * 0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8 + if sign then + sign = -1 + else + sign = 1 + end + local number + if mantissa == 0 and exponent == 0 then + number = sign * 0.0 + elseif exponent == 0x7FF then + if mantissa == 0 then + number = sign * math.huge + else + number = 0.0/0.0 + end + else + number = sign * math_ldexp(1.0 + mantissa / 0x10000000000000, exponent - 0x3FF) + end + return number + end +} diff --git a/src/cassandra/types/float.lua b/src/cassandra/types/float.lua new file mode 100644 index 0000000..f6df85c --- /dev/null +++ b/src/cassandra/types/float.lua @@ -0,0 +1,63 @@ +local string_char = string.char +local string_byte = string.byte +local math_ldexp = math.ldexp +local math_frexp = math.frexp +local math_floor = math.floor + +return { + repr = function(self, number) + if number == 0 then + return string_char(0x00, 0x00, 0x00, 0x00) + elseif number ~= number then + return string_char(0xFF, 0xFF, 0xFF, 0xFF) + else + local sign = 0x00 + if number < 0 then + sign = 0x80 + number = -number + end + local mantissa, exponent = math_frexp(number) + exponent = exponent + 0x7F + if exponent <= 0 then + mantissa = math_ldexp(mantissa, exponent - 1) + exponent = 0 + elseif exponent > 0 then + if exponent >= 0xFF then + return string_char(sign + 0x7F, 0x80, 0x00, 0x00) + elseif exponent == 1 then + exponent = 0 + else + mantissa = mantissa * 2 - 1 + exponent = exponent - 1 + end + end + mantissa = math_floor(math_ldexp(mantissa, 23) + 0.5) + return string_char(sign + math_floor(exponent / 2), + (exponent % 2) * 0x80 + math_floor(mantissa / 0x10000), + math_floor(mantissa / 0x100) % 0x100, + mantissa % 0x100) + end + end, + read = function(buffer) + local bytes = buffer:read(4) + local b1, b2, b3, b4 = string_byte(bytes, 1, 4) + local exponent = (b1 % 0x80) * 0x02 + math_floor(b2 / 0x80) + local mantissa = math_ldexp(((b2 % 0x80) * 0x100 + b3) * 0x100 + b4, -23) + if exponent == 0xFF then + if mantissa > 0 then + return 0 / 0 + else + mantissa = math.huge + exponent = 0x7F + end + elseif exponent > 0 then + mantissa = mantissa + 1 + else + exponent = exponent + 1 + end + if b1 >= 0x80 then + mantissa = -mantissa + end + return math_ldexp(mantissa, exponent - 0x7F) + end +} diff --git a/src/cassandra/types/options.lua b/src/cassandra/types/options.lua index 5064c3d..b21babf 100644 --- a/src/cassandra/types/options.lua +++ b/src/cassandra/types/options.lua @@ -5,7 +5,7 @@ return { read = function(buffer) local type_id = buffer:read_short() local type_value - if type_id == types.cql_types.set then + if type_id == types.cql_types.set or type_id == types.cql_types.list then type_value = buffer:read_options() elseif type_id == types.cql_types.map then type_value = {buffer:read_options(), buffer:read_options()} From 47935254b9023f5824049fbdacc791aae6d0acb2 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Wed, 18 Nov 2015 22:55:21 -0800 Subject: [PATCH 34/78] feat(rewrite) implement missing CQL types + tests --- spec/integration/cql_types_spec.lua | 65 ++++++++++++++++++++++++++++- spec/spec_utils.lua | 29 +++++++++++-- spec/unit/requests_spec.lua | 1 - src/cassandra/buffer.lua | 10 +++-- src/cassandra/frame_reader.lua | 4 +- src/cassandra/types/options.lua | 6 ++- src/cassandra/types/tuple.lua | 18 ++++++++ src/cassandra/types/tuple_type.lua | 14 +++++++ src/cassandra/types/udt.lua | 19 +++++++++ src/cassandra/types/udt_type.lua | 20 +++++++++ 10 files changed, 174 insertions(+), 12 deletions(-) create mode 100644 src/cassandra/types/tuple.lua create mode 100644 src/cassandra/types/tuple_type.lua create mode 100644 src/cassandra/types/udt.lua create mode 100644 src/cassandra/types/udt_type.lua diff --git a/spec/integration/cql_types_spec.lua b/spec/integration/cql_types_spec.lua index dbc78a3..986b5e1 100644 --- a/spec/integration/cql_types_spec.lua +++ b/spec/integration/cql_types_spec.lua @@ -25,6 +25,16 @@ describe("CQL types integration", function() _, err = session:set_keyspace(_KEYSPACE) assert.falsy(err) + _, err = session:execute [[ + CREATE TYPE address( + street text, + city text, + zip int, + country text + ) + ]] + assert.falsy(err) + _, err = session:execute [[ CREATE TABLE IF NOT EXISTS all_types( id uuid PRIMARY KEY, @@ -44,9 +54,12 @@ describe("CQL types integration", function() inet_sample inet, list_sample_text list, list_sample_int list, - map_sample map, + map_sample_text_text map, + map_sample_text_int map, set_sample_text set, - set_sample_int set + set_sample_int set, + udt_sample frozen
, + tuple_sample tuple ) ]] assert.falsy(err) @@ -82,6 +95,24 @@ describe("CQL types integration", function() end it("[list] should be inserted and retrieved", function() + for _, fixture in ipairs(utils.cql_map_fixtures) do + local insert_query = string.format("INSERT INTO all_types(id, map_sample_%s_%s) VALUES(?, ?)", fixture.key_type_name, fixture.value_type_name) + local select_query = string.format("SELECT map_sample_%s_%s FROM all_types WHERE id = ?", fixture.key_type_name, fixture.value_type_name) + + local res, err = session:execute(insert_query, {cassandra.uuid(_UUID), cassandra.map(fixture.value)}) + assert.falsy(err) + assert.truthy(res) + + local rows, err = session:execute(select_query, {cassandra.uuid(_UUID)}) + assert.falsy(err) + assert.truthy(rows) + + local decoded = rows[1]["map_sample_"..fixture.key_type_name.."_"..fixture.value_type_name] + assert.validFixture("list", fixture.value, decoded) + end + end) + + it("[map] should be inserted and retrieved", function() for _, fixture in ipairs(utils.cql_list_fixtures) do local insert_query = string.format("INSERT INTO all_types(id, list_sample_%s) VALUES(?, ?)", fixture.type_name) local select_query = string.format("SELECT list_sample_%s FROM all_types WHERE id = ?", fixture.type_name) @@ -116,4 +147,34 @@ describe("CQL types integration", function() assert.sameSet(fixture.value, decoded) end end) + + it("[udt] should be inserted and retrieved", function() + local res, err = session:execute("INSERT INTO all_types(id, udt_sample) VALUES(?, ?)", + {cassandra.uuid(_UUID), cassandra.udt({"montgomery st", "san francisco", 94111, nil})}) + assert.falsy(err) + assert.truthy(res) + + local rows, err = session:execute("SELECT udt_sample FROM all_types WHERE id = ?", {cassandra.uuid(_UUID)}) + assert.falsy(err) + assert.truthy(rows) + local address = rows[1].udt_sample + assert.equal("montgomery st", address.street) + assert.equal("san francisco", address.city) + assert.equal(94111, address.zip) + end) + + it("[tuple] should be inserted and retrieved", function() + for _, fixture in ipairs(utils.cql_tuple_fixtures) do + local res, err = session:execute("INSERT INTO all_types(id, tuple_sample) VALUES(?, ?)", {cassandra.uuid(_UUID), cassandra.tuple(fixture.value)}) + assert.falsy(err) + assert.truthy(res) + + local rows, err = session:execute("SELECT tuple_sample FROM all_types WHERE id = ?", {cassandra.uuid(_UUID)}) + assert.falsy(err) + assert.truthy(rows) + local tuple = rows[1].tuple_sample + assert.equal(fixture.value[1], tuple[1]) + assert.equal(fixture.value[2], tuple[2]) + end + end) end) diff --git a/spec/spec_utils.lua b/spec/spec_utils.lua index 1a54774..f305a90 100644 --- a/spec/spec_utils.lua +++ b/spec/spec_utils.lua @@ -111,13 +111,36 @@ _M.cql_list_fixtures = { } _M.cql_map_fixtures = { - {key_type = types.cql_types.text, value_type = types.cql_types.text, value = {k1 = "v1", k2 = "v2"}}, - {key_type = types.cql_types.text, value_type = types.cql_types.int, value = {k1 = 1, k2 = 2}}, - {key_type = types.cql_types.text, value_type = types.cql_types.int, value = {}} + { + key_type = types.cql_types.text, + key_type_name = "text", + value_type = types.cql_types.text, + value_type_name = "text", + value = {k1 = "v1", k2 = "v2"} + }, + { + key_type = types.cql_types.text, + key_type_name = "text", + value_type = types.cql_types.int, + value_type_name = "int", + value = {k1 = 1, k2 = 2} + }, + { + key_type = types.cql_types.text, + key_type_name = "text", + value_type = types.cql_types.int, + value_type_name = "int", + value = {} + } } _M.cql_set_fixtures = _M.cql_list_fixtures +_M.cql_tuple_fixtures = { + {type = {"text", "text"}, value = {"hello", "world"}}, + {type = {"text", "text"}, value = {"world", "hello"}} +} + _M.contact_points = {"127.0.0.1", "127.0.0.2"} return _M diff --git a/spec/unit/requests_spec.lua b/spec/unit/requests_spec.lua index 7f01b75..f117a25 100644 --- a/spec/unit/requests_spec.lua +++ b/spec/unit/requests_spec.lua @@ -1,7 +1,6 @@ local types = require "cassandra.types" local Buffer = require "cassandra.buffer" local requests = require "cassandra.requests" -local frame_header = require "cassandra.types.frame_header" describe("Requests", function() describe("StartupRequest", function() diff --git a/src/cassandra/buffer.lua b/src/cassandra/buffer.lua index 414fe76..914733a 100644 --- a/src/cassandra/buffer.lua +++ b/src/cassandra/buffer.lua @@ -26,6 +26,10 @@ local TYPES = { -- "consistency" "string_map", -- "string_multimap" + + -- type decoders + "udt_type", + "tuple_type" } for _, buf_type in ipairs(TYPES) do @@ -62,8 +66,8 @@ local CQL_DECODERS = { [cql_types.varchar] = "raw", [cql_types.varint] = "int", [cql_types.timeuuid] = "uuid", - -- [cql_types.udt] = "udt", - -- [cql_types.tuple] = "tuple" + [cql_types.udt] = "udt", + [cql_types.tuple] = "tuple" } local ALIASES = { @@ -107,7 +111,7 @@ function Buffer:repr_cql_value(value) if math_floor(value) == value then infered_type = cql_types.int else - --infered_type = cql_types.float + infered_type = cql_types.float end elseif lua_type == "table" then if t_utils.is_array(value) then diff --git a/src/cassandra/frame_reader.lua b/src/cassandra/frame_reader.lua index 732272c..889d0a6 100644 --- a/src/cassandra/frame_reader.lua +++ b/src/cassandra/frame_reader.lua @@ -47,7 +47,7 @@ local function parse_metadata(buffer) t_name = buffer:read_string() end local col_name = buffer:read_string() - local col_type = buffer:read_options() -- {type_id = ...[, value_type_id = ...]} + local col_type = buffer:read_options() columns[#columns + 1] = { name = col_name, type = col_type, @@ -78,6 +78,8 @@ local RESULT_PARSERS = { for _ = 1, rows_count do local row = {} for i = 1, columns_count do + local inspect = require "inspect" + --print(inspect(columns[i].type)) --print("reading column "..columns[i].name) local value = buffer:read_cql_value(columns[i].type) --local inspect = require "inspect" diff --git a/src/cassandra/types/options.lua b/src/cassandra/types/options.lua index b21babf..4e021c4 100644 --- a/src/cassandra/types/options.lua +++ b/src/cassandra/types/options.lua @@ -1,6 +1,5 @@ local types = require "cassandra.types" - return { read = function(buffer) local type_id = buffer:read_short() @@ -9,9 +8,12 @@ return { type_value = buffer:read_options() elseif type_id == types.cql_types.map then type_value = {buffer:read_options(), buffer:read_options()} + elseif type_id == types.cql_types.udt then + type_value = buffer:read_udt_type() + elseif type_id == types.cql_types.tuple then + type_value = buffer:read_tuple_type() end - -- @TODO support non-native types (custom, map, list, set, UDT, tuple) return {type_id = type_id, value_type_id = type_value} end } diff --git a/src/cassandra/types/tuple.lua b/src/cassandra/types/tuple.lua new file mode 100644 index 0000000..990028d --- /dev/null +++ b/src/cassandra/types/tuple.lua @@ -0,0 +1,18 @@ +local table_concat = table.concat + +return { + repr = function(self, values) + local repr = {} + for _, v in ipairs(values) do + repr[#repr + 1] = self:repr_cql_value(v) + end + return table_concat(repr) + end, + read = function(buffer, type) + local tuple = {} + for _, field in ipairs(type.fields) do + tuple[#tuple + 1] = buffer:read_cql_value(field.type) + end + return tuple + end +} diff --git a/src/cassandra/types/tuple_type.lua b/src/cassandra/types/tuple_type.lua new file mode 100644 index 0000000..bdad6c5 --- /dev/null +++ b/src/cassandra/types/tuple_type.lua @@ -0,0 +1,14 @@ +return { + read = function(buffer, type) + local n = buffer:read_short() + local fields = {} + for _ = 1, n do + fields[#fields + 1] = { + type = buffer:read_options() + } + end + return { + fields = fields + } + end +} diff --git a/src/cassandra/types/udt.lua b/src/cassandra/types/udt.lua new file mode 100644 index 0000000..91ab976 --- /dev/null +++ b/src/cassandra/types/udt.lua @@ -0,0 +1,19 @@ +local table_concat = table.concat + +return { + -- values must be ordered as they are defined in the UDT declaration + repr = function(self, values) + local repr = {} + for _, v in ipairs(values) do + repr[#repr + 1] = self:repr_cql_value(v) + end + return table_concat(repr) + end, + read = function(buffer, type) + local udt = {} + for _, field in ipairs(type.fields) do + udt[field.name] = buffer:read_cql_value(field.type) + end + return udt + end +} diff --git a/src/cassandra/types/udt_type.lua b/src/cassandra/types/udt_type.lua new file mode 100644 index 0000000..aa37f71 --- /dev/null +++ b/src/cassandra/types/udt_type.lua @@ -0,0 +1,20 @@ +return { + read = function(buffer) + local udt_ks_name = buffer:read_string() + local udt_name = buffer:read_string() + + local n = buffer:read_short() + local fields = {} + for _ = 1, n do + fields[#fields + 1] = { + name = buffer:read_string(), + type = buffer:read_options() + } + end + return { + udt_name = udt_name, + udt_keyspace = udt_ks_name, + fields = fields + } + end +} From 026d633fc368bf0e3765eb1075daa1571782680c Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Thu, 19 Nov 2015 14:28:30 -0800 Subject: [PATCH 35/78] feat(rewrite) pagination + tests --- spec/integration/cassandra_spec.lua | 54 +++++++++++++++++++++++++---- src/cassandra/buffer.lua | 4 +-- src/cassandra/frame_reader.lua | 21 +++++------ 3 files changed, 60 insertions(+), 19 deletions(-) diff --git a/spec/integration/cassandra_spec.lua b/spec/integration/cassandra_spec.lua index 33e5e71..49d4954 100644 --- a/spec/integration/cassandra_spec.lua +++ b/spec/integration/cassandra_spec.lua @@ -189,9 +189,10 @@ describe("session", function() local _, err = session:execute [[ CREATE TABLE IF NOT EXISTS resty_cassandra_specs.users( - id uuid PRIMARY KEY, + id uuid, name varchar, - age int + n int, + PRIMARY KEY(id, n) ) ]] assert.falsy(err) @@ -224,8 +225,8 @@ describe("session", function() describe(":execute()", function() it("should accept values to bind", function() - local res, err = session:execute("INSERT INTO users(id, name, age) VALUES(?, ?, ?)", - {cassandra.uuid("2644bada-852c-11e3-89fb-e0b9a54a6d93"), "Bob", 42}) + local res, err = session:execute("INSERT INTO users(id, name, n) VALUES(?, ?, ?)", + {cassandra.uuid("2644bada-852c-11e3-89fb-e0b9a54a6d93"), "Bob", 1}) assert.falsy(err) assert.truthy(res) assert.equal("VOID", res.type) @@ -237,8 +238,8 @@ describe("session", function() assert.equal("Bob", rows[1].name) end) it("support somewhat heavier insertions", function() - for i = 1, 1000 do - local res, err = session:execute("INSERT INTO users(id, name, age) VALUES(uuid(), ?, ?)", {"Alice", 33}) + for i = 2, 10000 do + local res, err = session:execute("INSERT INTO users(id, name, n) VALUES(2644bada-852c-11e3-89fb-e0b9a54a6d93, ?, ?)", {"Alice", i}) assert.falsy(err) assert.truthy(res) end @@ -246,7 +247,46 @@ describe("session", function() local rows, err = session:execute("SELECT COUNT(*) FROM users") assert.falsy(err) assert.truthy(rows) - assert.equal(1001, rows[1].count) + assert.equal(10000, rows[1].count) + end) + it("should have a default page_size (5000)", function() + local rows, err = session:execute("SELECT * FROM users WHERE id = 2644bada-852c-11e3-89fb-e0b9a54a6d93 ORDER BY n") + assert.falsy(err) + assert.truthy(rows) + assert.truthy(rows.meta) + assert.True(rows.meta.has_more_pages) + assert.truthy(rows.meta.paging_state) + assert.equal(5000, #rows) + assert.equal(1, rows[1].n) + assert.equal(5000, rows[#rows].n) + end) + it("should be possible to specify a per-query page_size option", function() + local rows, err = session:execute("SELECT * FROM users WHERE id = 2644bada-852c-11e3-89fb-e0b9a54a6d93 ORDER BY n", nil, {page_size = 100}) + assert.falsy(err) + assert.truthy(rows) + assert.equal(100, #rows) + + local rows, err = session:execute("SELECT * FROM users") + assert.falsy(err) + assert.truthy(rows) + assert.equal(5000, #rows) + end) + it("should support passing a paging_state to retrieve next pages", function() + local rows, err = session:execute("SELECT * FROM users WHERE id = 2644bada-852c-11e3-89fb-e0b9a54a6d93 ORDER BY n", nil, {page_size = 100}) + assert.falsy(err) + assert.truthy(rows) + assert.equal(100, #rows) + assert.equal(1, rows[1].n) + assert.equal(100, rows[#rows].n) + + local paging_state = rows.meta.paging_state + + rows, err = session:execute("SELECT * FROM users WHERE id = 2644bada-852c-11e3-89fb-e0b9a54a6d93 ORDER BY n", nil, {page_size = 100, paging_state = paging_state}) + assert.falsy(err) + assert.truthy(rows) + assert.equal(100, #rows) + assert.equal(101, rows[1].n) + assert.equal(200, rows[#rows].n) end) end) diff --git a/src/cassandra/buffer.lua b/src/cassandra/buffer.lua index 914733a..2fd7c21 100644 --- a/src/cassandra/buffer.lua +++ b/src/cassandra/buffer.lua @@ -51,7 +51,7 @@ local CQL_DECODERS = { [cql_types.bigint] = "bigint", [cql_types.blob] = "raw", [cql_types.boolean] = "boolean", - -- [cql_types.counter] = "counter", + [cql_types.counter] = "bigint", -- decimal 0x06 [cql_types.double] = "double", [cql_types.float] = "float", @@ -72,7 +72,7 @@ local CQL_DECODERS = { local ALIASES = { raw = {"ascii", "blob", "text", "varchar"}, - bigint = {"timestamp"}, + bigint = {"timestamp", "counter"}, int = {"varint"}, set = {"list"}, uuid = {"timeuuid"} diff --git a/src/cassandra/frame_reader.lua b/src/cassandra/frame_reader.lua index 889d0a6..01d17a3 100644 --- a/src/cassandra/frame_reader.lua +++ b/src/cassandra/frame_reader.lua @@ -26,7 +26,7 @@ local ROWS_RESULT_FLAGS = { -- @section result_parser local function parse_metadata(buffer) - local k_name, t_name + local k_name, t_name, paging_state local flags = buffer:read_int() local columns_count = buffer:read_int() @@ -35,6 +35,10 @@ local function parse_metadata(buffer) local has_global_table_spec = bit.btest(flags, ROWS_RESULT_FLAGS.GLOBAL_TABLES_SPEC) local has_no_metadata = bit.btest(flags, ROWS_RESULT_FLAGS.NO_METADATA) + if has_more_pages then + paging_state = buffer:read_bytes() + end + if has_global_table_spec then k_name = buffer:read_string() t_name = buffer:read_string() @@ -58,7 +62,9 @@ local function parse_metadata(buffer) return { columns = columns, - columns_count = columns_count + columns_count = columns_count, + has_more_pages = has_more_pages, + paging_state = paging_state } end @@ -73,18 +79,13 @@ local RESULT_PARSERS = { local rows_count = buffer:read_int() local rows = { - type = "ROWS" + type = "ROWS", + meta = metadata } for _ = 1, rows_count do local row = {} for i = 1, columns_count do - local inspect = require "inspect" - --print(inspect(columns[i].type)) - --print("reading column "..columns[i].name) - local value = buffer:read_cql_value(columns[i].type) - --local inspect = require "inspect" - --print("column "..columns[i].name.." = "..inspect(value)) - row[columns[i].name] = value + row[columns[i].name] = buffer:read_cql_value(columns[i].type) end rows[#rows + 1] = row end From 772905b83026c378c8dd4834692f665abfdc7c8d Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Thu, 19 Nov 2015 14:51:07 -0800 Subject: [PATCH 36/78] feat(rewrite) auto_paging option and iterator --- spec/integration/cassandra_spec.lua | 48 +++++++++++++++++++++++++++++ src/cassandra.lua | 30 ++++++++++++++++++ src/cassandra/options.lua | 1 + 3 files changed, 79 insertions(+) diff --git a/spec/integration/cassandra_spec.lua b/spec/integration/cassandra_spec.lua index 49d4954..a1d4208 100644 --- a/spec/integration/cassandra_spec.lua +++ b/spec/integration/cassandra_spec.lua @@ -288,6 +288,54 @@ describe("session", function() assert.equal(101, rows[1].n) assert.equal(200, rows[#rows].n) end) + describe("auto_paging", function() + it("should return an iterator if given an `auto_paging` option", function() + local page_tracker = 0 + for rows, err, page in session:execute("SELECT * FROM users", nil, {page_size = 10, auto_paging = true}) do + assert.falsy(err) + page_tracker = page_tracker + 1 + assert.equal(page_tracker, page) + assert.equal(10, #rows) + end + + assert.equal(1000, page_tracker) + end) + it("should return the latest page of a set", function() + -- When the latest page contains only 1 element + local page_tracker = 0 + for rows, err, page in session:execute("SELECT * FROM users", nil, {page_size = 9999, auto_paging = true}) do + assert.falsy(err) + page_tracker = page_tracker + 1 + assert.equal(page_tracker, page) + end + + assert.equal(2, page_tracker) + + -- Even if all results are fetched in the first page + page_tracker = 0 + for rows, err, page in session:execute("SELECT * FROM users", nil, {page_size = 10000, auto_paging = true}) do + assert.falsy(err) + page_tracker = page_tracker + 1 + assert.equal(page_tracker, page) + assert.equal(10000, #rows) + end + + assert.same(1, page_tracker) + end) + it("should return any error", function() + -- This test validates the behaviour of err being returned if no + -- results are returned (most likely because of an invalid query) + local page_tracker = 0 + for rows, err, page in session:execute("SELECT * FROM users WHERE col = 500", nil, {auto_paging = true}) do + assert.truthy(err) -- 'col' is not a valid column + assert.equal(0, page) + page_tracker = page_tracker + 1 + end + + -- Assert the loop has been run once. + assert.equal(1, page_tracker) + end) + end) end) describe(":shutdown()", function() diff --git a/src/cassandra.lua b/src/cassandra.lua index 6686fae..3c51831 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -463,9 +463,39 @@ function Session:new(options) return setmetatable(s, {__index = self}) end +local function page_iterator(session, query, args, options) + local page = 0 + return function(query, previous_rows) + if previous_rows and previous_rows.meta.has_more_pages == false then + return nil -- End iteration after error + end + + options.auto_paging = false + options.paging_state = previous_rows and previous_rows.meta.paging_state + + local rows, err = session:execute(query, args, options) + + -- If we have some results, increment the page + if rows ~= nil and #rows > 0 then + page = page + 1 + else + if err then + -- Just expose the error with 1 last iteration + return {meta = {has_more_pages = false}}, err, page + elseif rows.meta.has_more_pages == false then + return nil -- End of the iteration + end + end + + return rows, err, page + end, query, nil +end + function Session:execute(query, args, options) if self.terminated then return nil, Errors.NoHostAvailableError(nil, "Cannot reuse a session that has been shut down.") + elseif options and options.auto_paging then + return page_iterator(self, query, args, options) end local q_options = table_utils.deep_copy(self.options) diff --git a/src/cassandra/options.lua b/src/cassandra/options.lua index 444b2ec..7298e08 100644 --- a/src/cassandra/options.lua +++ b/src/cassandra/options.lua @@ -19,6 +19,7 @@ local DEFAULTS = { serial_consistency = types.consistencies.serial, page_size = 5000, paging_state = nil, -- stub + auto_paging = false, prepare = false, retry_on_timeout = true }, From 07d0f54e54131ec92da1f646edac6106095b937e Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Thu, 19 Nov 2015 21:54:56 -0800 Subject: [PATCH 37/78] feat(rewrite) prepared queries - cluster-wide prepares - retry policy for failed prepared execution --- spec/page.lua | 47 ++++++++++++ spec/prepare.lua | 30 ++++++++ src/cassandra.lua | 105 +++++++++++++++++++++------ src/cassandra/buffer.lua | 2 +- src/cassandra/cache.lua | 33 +++++++++ src/cassandra/frame_reader.lua | 11 +++ src/cassandra/options.lua | 2 +- src/cassandra/requests.lua | 106 +++++++++++++++++++++------- src/cassandra/types/short_bytes.lua | 11 +++ 9 files changed, 297 insertions(+), 50 deletions(-) create mode 100644 spec/page.lua create mode 100644 spec/prepare.lua create mode 100644 src/cassandra/types/short_bytes.lua diff --git a/spec/page.lua b/spec/page.lua new file mode 100644 index 0000000..76151a5 --- /dev/null +++ b/spec/page.lua @@ -0,0 +1,47 @@ +package.path = "src/?.lua;src/?/init.lua;"..package.path +local inspect = require "inspect" +local cassandra = require "cassandra" +local log = require "cassandra.log" + +log.set_lvl("INFO") + +local _, err = cassandra.spawn_cluster {shm = "cassandra", contact_points = {"127.0.0.1", "127.0.0.2"}} +assert(err == nil, inspect(err)) + +local session, err = cassandra.spawn_session {shm = "cassandra"} +assert(err == nil, inspect(err)) + +local _, err = session:execute([[ + CREATE KEYSPACE IF NOT EXISTS page + WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor': 2} +]]) +assert(err == nil, inspect(err)) + +os.execute("sleep 1") + +local _, err = session:execute [[ + CREATE TABLE IF NOT EXISTS page.users( + id uuid PRIMARY KEY, + name varchar, + age int + ) +]] +assert(err == nil, inspect(err)) + +local _, err = session:set_keyspace("page") +assert(err == nil, inspect(err)) + +os.execute("sleep 1") + +for i = 1, 10000 do + --local _, err = session:execute("INSERT INTO users(id, name, age) VALUES(uuid(), ?, ?)", {"Alice", 33}) + --assert(err == nil, inspect(err)) +end + +local rows, err = session:execute("SELECT COUNT(*) FROM users") +assert(err == nil, inspect(err)) +print(rows[1].count) + +local rows, err = session:execute("SELECT * FROM users") +assert(err == nil, inspect(err)) +print(inspect(rows)) diff --git a/spec/prepare.lua b/spec/prepare.lua new file mode 100644 index 0000000..b129a3c --- /dev/null +++ b/spec/prepare.lua @@ -0,0 +1,30 @@ +package.path = "src/?.lua;src/?/init.lua;"..package.path +local inspect = require "inspect" +local cassandra = require "cassandra" +local log = require "cassandra.log" + +log.set_lvl("INFO") + +local _, err = cassandra.spawn_cluster {shm = "cassandra", contact_points = {"127.0.0.1", "127.0.0.2"}} +assert(err == nil, inspect(err)) + +local session, err = cassandra.spawn_session {shm = "cassandra", keyspace = "page"} +assert(err == nil, inspect(err)) + +-- +-- +-- + +local rows, err = session:execute("SELECT * FROM users", nil, {prepare = true}) +if err then + error(err) +end + +print(#rows) + +local rows, err = session:execute("SELECT * FROM users", nil, {prepare = true}) +if err then + error(err) +end + +print(#rows) diff --git a/src/cassandra.lua b/src/cassandra.lua index 3c51831..1f1f05e 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -14,6 +14,7 @@ local FrameReader = require "cassandra.frame_reader" local table_insert = table.insert local string_find = string.find +local string_format = string.format local CQL_Errors = types.ERRORS --- Host @@ -303,10 +304,9 @@ end local RequestHandler = {} -function RequestHandler:new(request, hosts, options) +function RequestHandler:new(hosts, options) local o = { hosts = hosts, - request = request, options = options, n_retries = 0 } @@ -360,7 +360,7 @@ function RequestHandler:get_next_coordinator() return nil, Errors.NoHostAvailableError(errors) end -function RequestHandler:send() +function RequestHandler:send_on_next_coordinator(request) local coordinator, err = self:get_next_coordinator() if err then return nil, err @@ -368,18 +368,26 @@ function RequestHandler:send() log.info("Acquired connection through load balancing policy: "..coordinator.address) - local result, err = coordinator:send(self.request) + return self:send(request) +end - if coordinator.socket_type == "ngx" then - coordinator:set_keep_alive() +function RequestHandler:send(request) + if self.coordinator == nil then + return self:send_on_next_coordinator(request) + end + + local result, err = self.coordinator:send(request) + + if self.coordinator.socket_type == "ngx" then + self.coordinator:set_keep_alive() end if err then - return self:handle_error(err) + return self:handle_error(request, err) end -- Success! Make sure to re-up node in case it was marked as DOWN - local ok, cache_err = coordinator:set_up() + local ok, cache_err = self.coordinator:set_up() if not ok then return nil, cache_err end @@ -387,7 +395,7 @@ function RequestHandler:send() return result end -function RequestHandler:handle_error(err) +function RequestHandler:handle_error(request, err) local retry_policy = self.options.policies.retry local decision = retry_policy.decisions.throw @@ -398,20 +406,20 @@ function RequestHandler:handle_error(err) return nil, cache_err end -- always retry, another node will be picked - return self:retry() + return self:retry(request) elseif err.type == "TimeoutError" then if self.options.query_options.retry_on_timeout then - return self:retry() + return self:retry(request) end elseif err.type == "ResponseError" then local request_infos = { handler = self, - request = self.request, + request = request, n_retries = self.n_retries } if err.code == CQL_Errors.OVERLOADED or err.code == CQL_Errors.IS_BOOTSTRAPPING or err.code == CQL_Errors.TRUNCATE_ERROR then -- always retry, we will hit another node - return self:retry() + return self:retry(request) elseif err.code == CQL_Errors.UNAVAILABLE_EXCEPTION then decision = retry_policy.on_unavailable(request_infos) elseif err.code == CQL_Errors.READ_TIMEOUT then @@ -419,22 +427,46 @@ function RequestHandler:handle_error(err) elseif err.code == CQL_Errors.WRITE_TIMEOUT then decision = retry_policy.on_write_timeout(request_infos) elseif err.code == CQL_Errors.UNPREPARED then - -- re-prepare and retry + return self:prepare_and_retry(request) end end if decision == retry_policy.decisions.retry then - return self:retry() + return self:retry(request) end -- this error needs to be reported to the session return nil, err end -function RequestHandler:retry() +function RequestHandler:retry(request) self.n_retries = self.n_retries + 1 log.info("Retrying request") - return self:send() + return self:send_on_next_coordinator(request) +end + +function RequestHandler:prepare_and_retry(request) + log.info("Query 0x"..request:hex_query_id().." not prepared on host "..self.coordinator.address..". Preparing and retrying.") + local query = request.query + local prepare_request = Requests.PrepareRequest(query) + local res, err = self:send(prepare_request) + if err then + return nil, err + end + log.info("Query prepared for host "..self.coordinator.address) + + if request.query_id ~= res.query_id then + log.warn(string_format("Unexpected difference between query ids for query %s (%s ~= %s)", query, request.query_id, res.query_id)) + request.query_id = res.query_id + end + + local ok, cache_err = cache.set_prepared_query_id(self.options, query, res.query_id) + if not ok then + return nil, cache_err + end + + -- Send on the same coordinator as the one it was just prepared on + return self:send(request) end --- Session @@ -491,19 +523,49 @@ local function page_iterator(session, query, args, options) end, query, nil end +local function prepare_and_execute(self, query, args, options) + local request_handler = RequestHandler:new(self.hosts, self.options) + local query_id, cache_err = cache.get_prepared_query_id(self.options, query) + if cache_err then + return nil, cache_err + elseif query_id == nil then + log.info("Query not prepared in cluster yet. Preparing.") + local prepare_request = Requests.PrepareRequest(query) + local res, err = request_handler:send(prepare_request) + if err then + return nil, err + end + + query_id = res.query_id + local ok, cache_err = cache.set_prepared_query_id(self.options, query, query_id) + if not ok then + return nil, cache_err + end + log.info("Query prepared for host "..request_handler.coordinator.address) + end + + -- Send on the same coordinator as the one it was just prepared on + local prepared_request = Requests.ExecutePreparedRequest(query_id, query, args, options) + return request_handler:send(prepared_request) +end + function Session:execute(query, args, options) if self.terminated then return nil, Errors.NoHostAvailableError(nil, "Cannot reuse a session that has been shut down.") - elseif options and options.auto_paging then - return page_iterator(self, query, args, options) end local q_options = table_utils.deep_copy(self.options) q_options.query_options = table_utils.extend_table(q_options.query_options, options) + if q_options.query_options.auto_paging then + return page_iterator(self, query, args, options) + elseif q_options.query_options.prepare then + return prepare_and_execute(self, query, args, options) + end + local query_request = Requests.QueryRequest(query, args, q_options.query_options) - local request_handler = RequestHandler:new(query_request, self.hosts, q_options) - return request_handler:send() + local request_handler = RequestHandler:new(self.hosts, q_options) + return request_handler:send_on_next_coordinator(query_request) end function Session:set_keyspace(keyspace) @@ -593,6 +655,7 @@ function Cassandra.refresh_hosts(contact_points_hosts, options) } end log.info("Peers info retrieved") + log.info(string_format("---- cluster spawned under shm %s ----", options.shm)) coordinator:close() diff --git a/src/cassandra/buffer.lua b/src/cassandra/buffer.lua index 2fd7c21..683f2c6 100644 --- a/src/cassandra/buffer.lua +++ b/src/cassandra/buffer.lua @@ -19,7 +19,7 @@ local TYPES = { "uuid", -- "string_list", "bytes", - -- "short_bytes", + "short_bytes", "options", -- "options_list" "inet", diff --git a/src/cassandra/cache.lua b/src/cassandra/cache.lua index 95ce369..5f13210 100644 --- a/src/cassandra/cache.lua +++ b/src/cassandra/cache.lua @@ -121,10 +121,43 @@ local function get_host(shm, host_addr) return json.decode(value) end +--- Prepared query ids +-- @section prepared_query_ids + +local function key_for_prepared_query(keyspace, query) + return (keyspace or "").."_"..query +end + +local function set_prepared_query_id(options, query, query_id) + local shm = options.shm + local dict = get_dict(shm) + local prepared_key = key_for_prepared_query(options.keyspace, query) + + local ok, err = dict:set(prepared_key, query_id) + if not ok then + err = "Cannot store prepared query id for cluster "..shm..": "..err + end + return ok, err +end + +local function get_prepared_query_id(options, query) + local shm = options.shm + local dict = get_dict(shm) + local prepared_key = key_for_prepared_query(options.keyspace, query) + + local value, err = dict:get(prepared_key) + if err then + err = "Cannot retrieve prepared query id for cluster "..shm..": "..err + end + return value, err +end + return { get_dict = get_dict, get_host = get_host, set_host = set_host, set_hosts = set_hosts, get_hosts = get_hosts, + set_prepared_query_id = set_prepared_query_id, + get_prepared_query_id = get_prepared_query_id } diff --git a/src/cassandra/frame_reader.lua b/src/cassandra/frame_reader.lua index 01d17a3..e56048e 100644 --- a/src/cassandra/frame_reader.lua +++ b/src/cassandra/frame_reader.lua @@ -98,6 +98,17 @@ local RESULT_PARSERS = { keyspace = buffer:read_string() } end, + [RESULT_KINDS.PREPARED] = function(buffer) + local query_id = buffer:read_short_bytes() + local metadata = parse_metadata(buffer) + local result_metadata = parse_metadata(buffer) + return { + type = "PREPARED", + query_id = query_id, + meta = metadata, + result = result_metadata + } + end, [RESULT_KINDS.SCHEMA_CHANGE] = function(buffer) return { type = "SCHEMA_CHANGE", diff --git a/src/cassandra/options.lua b/src/cassandra/options.lua index 7298e08..1132788 100644 --- a/src/cassandra/options.lua +++ b/src/cassandra/options.lua @@ -5,7 +5,7 @@ local utils = require "cassandra.utils.table" -- @section defaults local DEFAULTS = { - shm = nil, -- required + shm = nil, -- stub contact_points = {}, keyspace = nil, -- stub policies = { diff --git a/src/cassandra/requests.lua b/src/cassandra/requests.lua index 2daf0fe..61801ce 100644 --- a/src/cassandra/requests.lua +++ b/src/cassandra/requests.lua @@ -7,6 +7,7 @@ local FrameHeader = require "cassandra.types.frame_header" local OP_CODES = types.OP_CODES local string_format = string.format +local string_byte = string.byte --- Query Flags -- @section query_flags @@ -80,6 +81,38 @@ end --- QueryRequest -- @section query_request +local function build_request_parameters(frame_body, version, params, options) + -- v2: [...][][][] + -- v3: [[name_1]...[name_n]][][][][] + + if options.consistency == nil then + options.consistency = types.consistencies.one + end + + local flags = 0x00 + local flags_buffer = Buffer(version) + if params ~= nil then + flags = bit.bor(flags, query_flags.values) + flags_buffer:write_cql_values(params) + end + if options.page_size ~= nil then + flags = bit.bor(flags, query_flags.page_size) + flags_buffer:write_int(options.page_size) + end + if options.paging_state ~= nil then + flags = bit.bor(flags, query_flags.paging_state) + flags_buffer:write_bytes(options.paging_state) + end + if options.serial_consistency ~= nil then + flags = bit.bor(flags, query_flags.serial_consistency) + flags_buffer:write_short(options.serial_consistency) + end + + frame_body:write_short(options.consistency) + frame_body:write_byte(flags) + frame_body:write(flags_buffer:dump()) +end + local QueryRequest = Request:extend() function QueryRequest:new(query, params, options) @@ -94,33 +127,9 @@ function QueryRequest:build() -- [...][][][] -- v3: -- [[name_1]...[name_n]][][][][] - if self.options.consistency == nil then - self.options.consistency = types.consistencies.one - end - - local flags = 0x00 - local flags_buffer = Buffer(self.version) - if self.params ~= nil then - flags = bit.bor(flags, query_flags.values) - flags_buffer:write_cql_values(self.params) - end - if self.options.page_size ~= nil then - flags = bit.bor(flags, query_flags.page_size) - flags_buffer:write_int(self.options.page_size) - end - if self.options.paging_state ~= nil then - flags = bit.bor(flags, query_flags.paging_state) - flags_buffer:write_bytes(self.options.paging_state) - end - if self.options.serial_consistency ~= nil then - flags = bit.bor(flags, query_flags.serial_consistency) - flags_buffer:write_short(self.options.serial_consistency) - end self.frame_body:write_long_string(self.query) - self.frame_body:write_short(self.options.consistency) - self.frame_body:write_byte(flags) - self.frame_body:write(flags_buffer:dump()) + build_request_parameters(self.frame_body, self.version, self.params, self.options) end --- KeyspaceRequest @@ -133,8 +142,51 @@ function KeyspaceRequest:new(keyspace) KeyspaceRequest.super.new(self, query) end +--- PrepareRequest +-- @section prepare_request + +local PrepareRequest = Request:extend() + +function PrepareRequest:new(query) + self.query = query + QueryRequest.super.new(self, OP_CODES.PREPARE) +end + +function PrepareRequest:build() + self.frame_body:write_long_string(self.query) +end + +--- ExecutePreparedRequest +-- @section execute_prepared_request + +local ExecutePreparedRequest = Request:extend() + +function ExecutePreparedRequest:new(query_id, query, params, options) + self.query_id = query_id + self.query = query + self.params = params + self.options = options + ExecutePreparedRequest.super.new(self, OP_CODES.EXECUTE) +end + +function ExecutePreparedRequest:build() + -- v2: + -- [...][][][] + -- v3: + -- [[name_1]...[name_n]][][][][] + + self.frame_body:write_short_bytes(self.query_id) + build_request_parameters(self.frame_body, self.version, self.params, self.options) +end + +function ExecutePreparedRequest:hex_query_id() + return bit.tohex(string_byte(self.query_id)) +end + return { - StartupRequest = StartupRequest, QueryRequest = QueryRequest, - KeyspaceRequest = KeyspaceRequest + StartupRequest = StartupRequest, + PrepareRequest = PrepareRequest, + KeyspaceRequest = KeyspaceRequest, + ExecutePreparedRequest = ExecutePreparedRequest } diff --git a/src/cassandra/types/short_bytes.lua b/src/cassandra/types/short_bytes.lua new file mode 100644 index 0000000..74fe91d --- /dev/null +++ b/src/cassandra/types/short_bytes.lua @@ -0,0 +1,11 @@ +local short = require "cassandra.types.short" + +return { + repr = function(self, val) + return short.repr(nil, #val)..val + end, + read = function(self) + local n_bytes = self:read_short() + return self:read(n_bytes) + end +} From b66380381c8e7fe43e15be1aca8c9d25753738ca Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Fri, 20 Nov 2015 14:21:50 -0800 Subject: [PATCH 38/78] perf(rewrite) inner_execute local function --- spec/integration/cassandra_spec.lua | 58 ++++++++++++++++ spec/prepare.lua | 31 +++++---- src/cassandra.lua | 102 ++++++++++++++++------------ src/cassandra/cache.lua | 5 +- 4 files changed, 136 insertions(+), 60 deletions(-) diff --git a/spec/integration/cassandra_spec.lua b/spec/integration/cassandra_spec.lua index a1d4208..0e55e40 100644 --- a/spec/integration/cassandra_spec.lua +++ b/spec/integration/cassandra_spec.lua @@ -336,6 +336,64 @@ describe("session", function() assert.equal(1, page_tracker) end) end) + describe("prepared queries", function() + it("should prepare a query before running it if given a `prepare` option", function() + local cache = require "cassandra.cache" + spy.on(cache, "get_prepared_query_id") + spy.on(cache, "set_prepared_query_id") + finally(function() + cache.get_prepared_query_id:revert() + cache.set_prepared_query_id:revert() + end) + + local rows, err = session:execute("SELECT * FROM users", nil, {prepare = true}) + assert.falsy(err) + assert.truthy(rows) + assert.True(#rows > 0) + + assert.spy(cache.get_prepared_query_id).was.called() + assert.spy(cache.set_prepared_query_id).was.called() + cache.get_prepared_query_id:clear() + cache.set_prepared_query_id:clear() + + -- again, and this time the query_id should be in the cache already + rows, err = session:execute("SELECT * FROM users", nil, {prepare = true}) + assert.falsy(err) + assert.truthy(rows) + assert.True(#rows > 0) + + assert.spy(cache.get_prepared_query_id).was.called() + assert.spy(cache.set_prepared_query_id).was.not_called() + end) + it("should support a heavier load of prepared queries", function() + for i = 1, 10000 do + local rows, err = session:execute("SELECT * FROM users", nil, {prepare = false, page_size = 10}) + assert.falsy(err) + assert.truthy(rows) + assert.True(#rows > 0) + end + end) + it("should be usable inside an `auto_paging` iterator", function() + local cache = require "cassandra.cache" + spy.on(cache, "get_prepared_query_id") + spy.on(cache, "set_prepared_query_id") + finally(function() + cache.get_prepared_query_id:revert() + cache.set_prepared_query_id:revert() + end) + + local page_tracker = 1 + for rows, err, page in session:execute("SELECT * FROM users", nil, {page_size = 10, auto_paging = true, prepare = true}) do + assert.falsy(err) + assert.truthy(rows) + assert.True(#rows > 0 and #rows <= 10) + page_tracker = page + end + + assert.spy(cache.get_prepared_query_id).was.called(page) + assert.spy(cache.set_prepared_query_id).was.called(0) + end) + end) end) describe(":shutdown()", function() diff --git a/spec/prepare.lua b/spec/prepare.lua index b129a3c..de284db 100644 --- a/spec/prepare.lua +++ b/spec/prepare.lua @@ -3,7 +3,7 @@ local inspect = require "inspect" local cassandra = require "cassandra" local log = require "cassandra.log" -log.set_lvl("INFO") +log.set_lvl("ERR") local _, err = cassandra.spawn_cluster {shm = "cassandra", contact_points = {"127.0.0.1", "127.0.0.2"}} assert(err == nil, inspect(err)) @@ -11,20 +11,27 @@ assert(err == nil, inspect(err)) local session, err = cassandra.spawn_session {shm = "cassandra", keyspace = "page"} assert(err == nil, inspect(err)) --- --- --- +-- for i = 1, 10000 do +-- local res, err = session:execute("INSERT INTO users(id, name, age) VALUES(uuid(), ?, ?)", {"Alice", i}) +-- if err then +-- error(err) +-- end +-- end + +local start, total + +start = os.clock() +for rows, err, page in session:execute("SELECT * FROM users", nil, {page_size = 20, auto_paging = true}) do -local rows, err = session:execute("SELECT * FROM users", nil, {prepare = true}) -if err then - error(err) end -print(#rows) +total = os.clock() - start +print("Time without prepared = "..total) + +start = os.clock() +for rows, err, page in session:execute("SELECT * FROM users", nil, {page_size = 20, auto_paging = true, prepare = true}) do -local rows, err = session:execute("SELECT * FROM users", nil, {prepare = true}) -if err then - error(err) end -print(#rows) +total = os.clock() - start +print("Time with prepared = "..total) diff --git a/src/cassandra.lua b/src/cassandra.lua index 1f1f05e..a9552dc 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -1,3 +1,14 @@ +-- @TODO +-- option for max prepared queries in cache +-- batches +-- prepared batches? +-- tracing +-- wait for schema consensus on SCHEMA_CHANGE results +-- +-- better logging +-- more options validation +-- more error types + local log = require "cassandra.log" local opts = require "cassandra.options" local types = require "cassandra.types" @@ -12,6 +23,7 @@ local string_utils = require "cassandra.utils.string" local FrameHeader = require "cassandra.types.frame_header" local FrameReader = require "cassandra.frame_reader" +local setmetatable = setmetatable local table_insert = table.insert local string_find = string.find local string_format = string.format @@ -460,11 +472,6 @@ function RequestHandler:prepare_and_retry(request) request.query_id = res.query_id end - local ok, cache_err = cache.set_prepared_query_id(self.options, query, res.query_id) - if not ok then - return nil, cache_err - end - -- Send on the same coordinator as the one it was just prepared on return self:send(request) end @@ -495,17 +502,50 @@ function Session:new(options) return setmetatable(s, {__index = self}) end -local function page_iterator(session, query, args, options) +local function prepare_and_execute(request_handler, query, args, query_options) + local query_id, cache_err = cache.get_prepared_query_id(request_handler.options, query) + if cache_err then + return nil, cache_err + elseif query_id == nil then + log.info("Query not prepared in cluster yet. Preparing.") + local prepare_request = Requests.PrepareRequest(query) + local res, err = request_handler:send(prepare_request) + if err then + return nil, err + end + + query_id = res.query_id + local ok, cache_err = cache.set_prepared_query_id(request_handler.options, query, query_id) + if not ok then + return nil, cache_err + end + log.info("Query prepared for host "..request_handler.coordinator.address) + end + + -- Send on the same coordinator as the one it was just prepared on + local prepared_request = Requests.ExecutePreparedRequest(query_id, query, args, query_options) + return request_handler:send(prepared_request) +end + +local function inner_execute(request_handler, query, args, query_options) + if query_options.prepare then + return prepare_and_execute(request_handler, query, args, query_options) + end + + local query_request = Requests.QueryRequest(query, args, query_options) + return request_handler:send_on_next_coordinator(query_request) +end + +local function page_iterator(request_handler, query, args, query_options) local page = 0 return function(query, previous_rows) if previous_rows and previous_rows.meta.has_more_pages == false then return nil -- End iteration after error end - options.auto_paging = false - options.paging_state = previous_rows and previous_rows.meta.paging_state + query_options.paging_state = previous_rows and previous_rows.meta.paging_state - local rows, err = session:execute(query, args, options) + local rows, err = inner_execute(request_handler, query, args, query_options) -- If we have some results, increment the page if rows ~= nil and #rows > 0 then @@ -523,49 +563,21 @@ local function page_iterator(session, query, args, options) end, query, nil end -local function prepare_and_execute(self, query, args, options) - local request_handler = RequestHandler:new(self.hosts, self.options) - local query_id, cache_err = cache.get_prepared_query_id(self.options, query) - if cache_err then - return nil, cache_err - elseif query_id == nil then - log.info("Query not prepared in cluster yet. Preparing.") - local prepare_request = Requests.PrepareRequest(query) - local res, err = request_handler:send(prepare_request) - if err then - return nil, err - end - - query_id = res.query_id - local ok, cache_err = cache.set_prepared_query_id(self.options, query, query_id) - if not ok then - return nil, cache_err - end - log.info("Query prepared for host "..request_handler.coordinator.address) - end - - -- Send on the same coordinator as the one it was just prepared on - local prepared_request = Requests.ExecutePreparedRequest(query_id, query, args, options) - return request_handler:send(prepared_request) -end - -function Session:execute(query, args, options) +function Session:execute(query, args, query_options) if self.terminated then return nil, Errors.NoHostAvailableError(nil, "Cannot reuse a session that has been shut down.") end - local q_options = table_utils.deep_copy(self.options) - q_options.query_options = table_utils.extend_table(q_options.query_options, options) + local options = table_utils.deep_copy(self.options) + options.query_options = table_utils.extend_table(options.query_options, query_options) + + local request_handler = RequestHandler:new(self.hosts, options) - if q_options.query_options.auto_paging then - return page_iterator(self, query, args, options) - elseif q_options.query_options.prepare then - return prepare_and_execute(self, query, args, options) + if options.query_options.auto_paging then + return page_iterator(request_handler, query, args, options.query_options) end - local query_request = Requests.QueryRequest(query, args, q_options.query_options) - local request_handler = RequestHandler:new(self.hosts, q_options) - return request_handler:send_on_next_coordinator(query_request) + return inner_execute(request_handler, query, args, options.query_options) end function Session:set_keyspace(keyspace) diff --git a/src/cassandra/cache.lua b/src/cassandra/cache.lua index 5f13210..4b656c1 100644 --- a/src/cassandra/cache.lua +++ b/src/cassandra/cache.lua @@ -141,13 +141,12 @@ local function set_prepared_query_id(options, query, query_id) end local function get_prepared_query_id(options, query) - local shm = options.shm - local dict = get_dict(shm) + local dict = get_dict(options.shm) local prepared_key = key_for_prepared_query(options.keyspace, query) local value, err = dict:get(prepared_key) if err then - err = "Cannot retrieve prepared query id for cluster "..shm..": "..err + err = "Cannot retrieve prepared query id for cluster "..options.shm..": "..err end return value, err end From 89c03cd695ea71fdf3d211772eaeb1525810008a Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Fri, 20 Nov 2015 17:23:20 -0800 Subject: [PATCH 39/78] feat(rewrite) batch statements (non-prepared) --- spec/integration/cassandra_spec.lua | 104 ++++++++++++++++++++++++++++ spec/unit/utils_spec.lua | 61 ++++++++++++++++ src/cassandra.lua | 9 +++ src/cassandra/buffer.lua | 2 +- src/cassandra/requests.lua | 53 +++++++++++++- src/cassandra/types/init.lua | 2 +- src/cassandra/types/long.lua | 11 +++ src/cassandra/utils/table.lua | 28 +++++--- 8 files changed, 259 insertions(+), 11 deletions(-) create mode 100644 src/cassandra/types/long.lua diff --git a/spec/integration/cassandra_spec.lua b/spec/integration/cassandra_spec.lua index 0e55e40..7cded7e 100644 --- a/spec/integration/cassandra_spec.lua +++ b/spec/integration/cassandra_spec.lua @@ -396,6 +396,110 @@ describe("session", function() end) end) + describe(":batch()", function() + local _UUID = "ca002f0a-8fe4-11e5-9663-43d80ec97d3e" + + setup(function() + local _, err = session:execute [[ + CREATE TABLE IF NOT EXISTS counter_test_table( + key text PRIMARY KEY, + value counter + ) + ]] + assert.falsy(err) + + utils.wait() + end) + + it("should execute logged batched queries with no params", function() + local res, err = session:batch({ + {"INSERT INTO users(id, name, n) VALUES(".._UUID..", 'Alice', 1)"}, + {"UPDATE users SET name = 'Alice' WHERE id = ".._UUID.." AND n = 1"}, + {"UPDATE users SET name = 'Alicia' WHERE id = ".._UUID.." AND n = 1"} + }) + assert.falsy(err) + assert.truthy(res) + assert.equal("VOID", res.type) + + local rows, err = session:execute("SELECT * FROM users WHERE id = ? AND n = 1", {cassandra.uuid(_UUID)}) + assert.falsy(err) + assert.truthy(rows) + local row = rows[1] + assert.equal("Alicia", row.name) + end) + it("should execute logged batched queries with params", function() + local res, err = session:batch({ + {"INSERT INTO users(id, name, n) VALUES(?, ?, ?)", {cassandra.uuid(_UUID), "Alice", 2}}, + {"UPDATE users SET name = ? WHERE id = ? AND n = 2", {"Alice", cassandra.uuid(_UUID)}}, + {"UPDATE users SET name = ? WHERE id = ? AND n = 2", {"Alicia2", cassandra.uuid(_UUID)}} + }) + assert.falsy(err) + assert.truthy(res) + assert.equal("VOID", res.type) + + local rows, err = session:execute("SELECT * FROM users WHERE id = ? AND n = 2", {cassandra.uuid(_UUID)}) + assert.falsy(err) + assert.truthy(rows) + local row = rows[1] + assert.equal("Alicia2", row.name) + end) + it("should execute unlogged batches", function() + local res, err = session:batch({ + {"INSERT INTO users(id, name, n) VALUES(?, ?, ?)", {cassandra.uuid(_UUID), "Alice", 3}}, + {"UPDATE users SET name = ? WHERE id = ? AND n = 3", {"Alice", cassandra.uuid(_UUID)}}, + {"UPDATE users SET name = ? WHERE id = ? AND n = 3", {"Alicia3", cassandra.uuid(_UUID)}} + }, {logged = false}) + assert.falsy(err) + assert.truthy(res) + assert.equal("VOID", res.type) + + local rows, err = session:execute("SELECT * FROM users WHERE id = ? AND n = 3", {cassandra.uuid(_UUID)}) + assert.falsy(err) + assert.truthy(rows) + local row = rows[1] + assert.equal("Alicia3", row.name) + end) + it("should execute counter batches", function() + local res, err = session:batch({ + {"UPDATE counter_test_table SET value = value + 1 WHERE key = 'counter'"}, + {"UPDATE counter_test_table SET value = value + 1 WHERE key = 'counter'"}, + {"UPDATE counter_test_table SET value = value + 1 WHERE key = ?", {"counter"}} + }, {counter = true}) + assert.falsy(err) + assert.truthy(res) + assert.equal("VOID", res.type) + + local rows, err = session:execute("SELECT value FROM counter_test_table WHERE key = 'counter'") + assert.falsy(err) + assert.truthy(rows) + local row = rows[1] + assert.equal(3, row.value) + end) + it("should return any error", function() + local res, err = session:batch({ + {"INSERT WHATEVER"}, + {"INSERT WHATEVER"} + }) + assert.truthy(err) + assert.equal("ResponseError", err.type) + end) + it("should support protocol level timestamp", function() + local res, err = session:batch({ + {"INSERT INTO users(id, name, n) VALUES(".._UUID..", 'Alice', 4)"}, + {"UPDATE users SET name = 'Alice' WHERE id = ".._UUID.." AND n = 4"}, + {"UPDATE users SET name = 'Alicia4' WHERE id = ".._UUID.." AND n = 4"} + }, {timestamp = 1428311323417123}) + assert.falsy(err) + + local rows, err = session:execute("SELECT name, writetime(name) FROM users WHERE id = ".._UUID.." AND n = 4") + assert.falsy(err) + assert.truthy(rows) + local row = rows[1] + assert.equal("Alicia4", row.name) + assert.equal(1428311323417123, row["writetime(name)"]) + end) + end) + describe(":shutdown()", function() session:shutdown() assert.True(session.terminated) diff --git a/spec/unit/utils_spec.lua b/spec/unit/utils_spec.lua index 685ff2b..6bc19e6 100644 --- a/spec/unit/utils_spec.lua +++ b/spec/unit/utils_spec.lua @@ -45,4 +45,65 @@ describe("table_utils", function() end) end) + describe("extend_table", function() + it("should extend a table from a source", function() + local source = {source = true} + local target = {target = true} + + target = table_utils.extend_table(source, target) + assert.True(target.target) + assert.True(target.source) + end) + it("should extend a table from multiple sources", function() + local source1 = {source1 = true} + local source2 = {source2 = true} + local target = {target = true} + + target = table_utils.extend_table(source1, source2, target) + assert.True(target.target) + assert.True(target.source1) + assert.True(target.source2) + end) + it("should extend nested properties", function() + local source1 = {source1 = true, source1_nested = {hello = "world"}} + local source2 = {source2 = true, source2_nested = {hello = "world"}} + local target = {target = true} + + target = table_utils.extend_table(source1, source2, target) + assert.True(target.target) + assert.True(target.source1) + assert.True(target.source2) + assert.truthy(target.source1_nested) + assert.truthy(target.source1_nested.hello) + assert.equal("world", target.source1_nested.hello) + assert.truthy(target.source2_nested) + assert.truthy(target.source2_nested.hello) + assert.equal("world", target.source2_nested.hello) + end) + it("should not override properties in the target", function() + local source = {source = true} + local target = {target = true, source = "source"} + + target = table_utils.extend_table(source, target) + assert.True(target.target) + assert.equal("source", target.source) + end) + it("should not override nested properties in the target", function() + local source1 = {source1 = true, source_nested = {hello = "world"}} + local target = {target = true, source_nested = {hello = "universe"}} + + target = table_utils.extend_table(source, target) + assert.True(target.target) + assert.truthy(target.source_nested) + assert.truthy(target.source_nested.hello) + assert.equal("universe", target.source_nested.hello) + end) + it("should not be mistaken by a `false` value", function() + local source = {source = true} + local target = {source = false} + + target = table_utils.extend_table(source, target) + assert.False(target.source) + end) + end) end) diff --git a/src/cassandra.lua b/src/cassandra.lua index a9552dc..8e4d413 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -580,6 +580,15 @@ function Session:execute(query, args, query_options) return inner_execute(request_handler, query, args, options.query_options) end +function Session:batch(queries, query_options) + local options = table_utils.deep_copy(self.options) + options.query_options = table_utils.extend_table({logged = true}, options.query_options, query_options) + + local request_handler = RequestHandler:new(self.hosts, options) + local batch_request = Requests.BatchRequest(queries, options.query_options) + return request_handler:send_on_next_coordinator(batch_request) +end + function Session:set_keyspace(keyspace) local errors = {} self.options.keyspace = keyspace diff --git a/src/cassandra/buffer.lua b/src/cassandra/buffer.lua index 683f2c6..6804fe7 100644 --- a/src/cassandra/buffer.lua +++ b/src/cassandra/buffer.lua @@ -12,7 +12,7 @@ local type = type local TYPES = { "byte", "int", - -- "long", + "long", "short", "string", "long_string", diff --git a/src/cassandra/requests.lua b/src/cassandra/requests.lua index 61801ce..5a941c9 100644 --- a/src/cassandra/requests.lua +++ b/src/cassandra/requests.lua @@ -183,10 +183,61 @@ function ExecutePreparedRequest:hex_query_id() return bit.tohex(string_byte(self.query_id)) end +--- BatchRequest +-- @section batch_request + +local BatchRequest = Request:extend() + +function BatchRequest:new(queries, options) + self.queries = queries + self.options = options + self.type = options.logged and 0 or 1 + self.type = options.counter and 2 or self.type + BatchRequest.super.new(self, OP_CODES.BATCH) +end + +function BatchRequest:build() + -- v2: ... + -- v3: ...[][] + + self.frame_body:write_byte(self.type) + self.frame_body:write_short(#self.queries) + + for _, q in ipairs(self.queries) do + local query, args = unpack(q) + -- only support non-prepared batches for now + self.frame_body:write_byte(0) + self.frame_body:write_long_string(query) + if args ~= nil then + self.frame_body:write_cql_values(args) + else + self.frame_body:write_short(0) + end + end + + self.frame_body:write_short(self.options.consistency) + + if self.version > 2 then + local flags = 0x00 + local flags_buffer = Buffer(self.version) + if self.options.serial_consistency ~= nil then + flags = bit.bor(flags, query_flags.serial_consistency) + flags_buffer:write_short(self.options.serial_consistency) + end + if self.options.timestamp ~= nil then + flags = bit.bor(flags, query_flags.default_timestamp) + flags_buffer:write_long(self.options.timestamp) + end + self.frame_body:write_byte(flags) + self.frame_body:write(flags_buffer:dump()) + end +end + return { QueryRequest = QueryRequest, StartupRequest = StartupRequest, PrepareRequest = PrepareRequest, KeyspaceRequest = KeyspaceRequest, - ExecutePreparedRequest = ExecutePreparedRequest + ExecutePreparedRequest = ExecutePreparedRequest, + BatchRequest = BatchRequest } diff --git a/src/cassandra/types/init.lua b/src/cassandra/types/init.lua index b79ded7..10054a3 100644 --- a/src/cassandra/types/init.lua +++ b/src/cassandra/types/init.lua @@ -89,7 +89,7 @@ local ERRORS_TRANSLATIONS = { [ERRORS.TRUNCATE_ERROR] = "Truncate error", [ERRORS.WRITE_TIMEOUT] = "Write timeout", [ERRORS.READ_TIMEOUT] = "Read timeout", - [ERRORS.SYNTAX_ERROR] = "Syntaxe rror", + [ERRORS.SYNTAX_ERROR] = "Syntax error", [ERRORS.UNAUTHORIZED] = "Unauthorized", [ERRORS.INVALID] = "Invalid", [ERRORS.CONFIG_ERROR] = "Config error", diff --git a/src/cassandra/types/long.lua b/src/cassandra/types/long.lua new file mode 100644 index 0000000..0266e36 --- /dev/null +++ b/src/cassandra/types/long.lua @@ -0,0 +1,11 @@ +local utils = require "cassandra.utils.number" + +return { + repr = function(self, val) + return utils.big_endian_representation(val, 8) + end, + read = function(buffer) + local bytes = buffer:read(8) + return utils.string_to_number(bytes, true) + end +} diff --git a/src/cassandra/utils/table.lua b/src/cassandra/utils/table.lua index 9e51a1b..7341da9 100644 --- a/src/cassandra/utils/table.lua +++ b/src/cassandra/utils/table.lua @@ -1,15 +1,27 @@ local CONSTS = require "cassandra.constants" +local setmetatable = setmetatable +local getmetatable = getmetatable +local rawget = rawget +local tostring = tostring +local pairs = pairs +local unpack = unpack +local type = type + local _M = {} -function _M.extend_table(defaults, values) - if values == nil then values = {} end - for k in pairs(defaults) do - if values[k] == nil then - values[k] = defaults[k] - end - if type(defaults[k]) == "table" then - _M.extend_table(defaults[k], values[k]) +function _M.extend_table(...) + local sources = {...} + local values = table.remove(sources) + + for _, source in ipairs(sources) do + for k in pairs(source) do + if values[k] == nil then + values[k] = source[k] + end + if type(source[k]) == "table" then + _M.extend_table(source[k], values[k]) + end end end From a4c0adb88ee9db4d4e8df84e5ba66ab4bf73f89f Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Fri, 20 Nov 2015 18:13:49 -0800 Subject: [PATCH 40/78] feat(rewrite) prepared batch statements --- spec/integration/cassandra_spec.lua | 46 ++++++++++++++++++++++++++++- src/cassandra.lua | 30 +++++++++++++++---- src/cassandra/requests.lua | 10 +++++-- 3 files changed, 76 insertions(+), 10 deletions(-) diff --git a/spec/integration/cassandra_spec.lua b/spec/integration/cassandra_spec.lua index 7cded7e..8de9e95 100644 --- a/spec/integration/cassandra_spec.lua +++ b/spec/integration/cassandra_spec.lua @@ -478,7 +478,7 @@ describe("session", function() it("should return any error", function() local res, err = session:batch({ {"INSERT WHATEVER"}, - {"INSERT WHATEVER"} + {"INSERT THING"} }) assert.truthy(err) assert.equal("ResponseError", err.type) @@ -498,6 +498,50 @@ describe("session", function() assert.equal("Alicia4", row.name) assert.equal(1428311323417123, row["writetime(name)"]) end) + it("should support serial consistency", function() + local res, err = session:batch({ + {"INSERT INTO users(id, name, n) VALUES(".._UUID..", 'Alice', 5)"}, + {"UPDATE users SET name = 'Alice' WHERE id = ".._UUID.." AND n = 5"}, + {"UPDATE users SET name = 'Alicia5' WHERE id = ".._UUID.." AND n = 5"} + }, {serial_consistency = cassandra.consistencies.local_serial}) + assert.falsy(err) + + local rows, err = session:execute("SELECT name, writetime(name) FROM users WHERE id = ".._UUID.." AND n = 5") + assert.falsy(err) + assert.truthy(rows) + local row = rows[1] + assert.equal("Alicia5", row.name) + end) + it("should support prepared queries in batch", function() + local cache = require "cassandra.cache" + spy.on(cache, "get_prepared_query_id") + spy.on(cache, "set_prepared_query_id") + finally(function() + cache.get_prepared_query_id:revert() + cache.set_prepared_query_id:revert() + end) + + local res, err = session:batch({ + {"INSERT INTO users(id, name, n) VALUES(?, ?, ?)", {cassandra.uuid(_UUID), "Alice", 6}}, + {"INSERT INTO users(id, name, n) VALUES(?, ?, ?)", {cassandra.uuid(_UUID), "Alice", 7}}, + {"UPDATE users SET name = ? WHERE id = ? AND n = ?", {"Alicia", cassandra.uuid(_UUID), 6}}, + {"UPDATE users SET name = ? WHERE id = ? AND n = ?", {"Alicia", cassandra.uuid(_UUID), 7}}, + {"UPDATE users SET name = ? WHERE id = ? AND n = ?", {"Alicia", cassandra.uuid(_UUID), 6}}, + {"UPDATE users SET name = ? WHERE id = ? AND n = ?", {"Alicia", cassandra.uuid(_UUID), 7}}, + {"UPDATE users SET name = ? WHERE id = ? AND n = ?", {"Alicia6", cassandra.uuid(_UUID), 6}}, + {"UPDATE users SET name = ? WHERE id = ? AND n = ?", {"Alicia7", cassandra.uuid(_UUID), 7}} + }, {prepare = true}) + assert.falsy(err) + + assert.spy(cache.get_prepared_query_id).was.called(8) + assert.spy(cache.set_prepared_query_id).was.called(2) + + local rows, err = session:execute("SELECT name FROM users WHERE id = ? AND n = ?", {cassandra.uuid(_UUID), 6}) + assert.falsy(err) + assert.truthy(rows) + local row = rows[1] + assert.equal("Alicia6", row.name) + end) end) describe(":shutdown()", function() diff --git a/src/cassandra.lua b/src/cassandra.lua index 8e4d413..7979fff 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -502,7 +502,7 @@ function Session:new(options) return setmetatable(s, {__index = self}) end -local function prepare_and_execute(request_handler, query, args, query_options) +local function prepare_query(request_handler, query) local query_id, cache_err = cache.get_prepared_query_id(request_handler.options, query) if cache_err then return nil, cache_err @@ -522,14 +522,19 @@ local function prepare_and_execute(request_handler, query, args, query_options) log.info("Query prepared for host "..request_handler.coordinator.address) end - -- Send on the same coordinator as the one it was just prepared on - local prepared_request = Requests.ExecutePreparedRequest(query_id, query, args, query_options) - return request_handler:send(prepared_request) + return query_id end local function inner_execute(request_handler, query, args, query_options) if query_options.prepare then - return prepare_and_execute(request_handler, query, args, query_options) + local query_id, err = prepare_query(request_handler, query) + if err then + return nil, err + end + + -- Send on the same coordinator as the one it was just prepared on + local prepared_request = Requests.ExecutePreparedRequest(query_id, query, args, query_options) + return request_handler:send(prepared_request) end local query_request = Requests.QueryRequest(query, args, query_options) @@ -585,8 +590,21 @@ function Session:batch(queries, query_options) options.query_options = table_utils.extend_table({logged = true}, options.query_options, query_options) local request_handler = RequestHandler:new(self.hosts, options) + + if options.query_options.prepare then + for i, q in ipairs(queries) do + local query_id, err = prepare_query(request_handler, q[1]) + if err then + return nil, err + end + queries[i].query_id = query_id + end + end + local batch_request = Requests.BatchRequest(queries, options.query_options) - return request_handler:send_on_next_coordinator(batch_request) + -- with :send(), the same coordinator will be used if we prepared some queries, + -- and a new one will be chosen if none were used yet. + return request_handler:send(batch_request) end function Session:set_keyspace(keyspace) diff --git a/src/cassandra/requests.lua b/src/cassandra/requests.lua index 5a941c9..47665cc 100644 --- a/src/cassandra/requests.lua +++ b/src/cassandra/requests.lua @@ -205,9 +205,13 @@ function BatchRequest:build() for _, q in ipairs(self.queries) do local query, args = unpack(q) - -- only support non-prepared batches for now - self.frame_body:write_byte(0) - self.frame_body:write_long_string(query) + if q.query_id ~= nil then + self.frame_body:write_byte(1) + self.frame_body:write_short_bytes(q.query_id) + else + self.frame_body:write_byte(0) + self.frame_body:write_long_string(query) + end if args ~= nil then self.frame_body:write_cql_values(args) else From ae60202b7fc9a2544f7c90214ec16a92ae435966 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Fri, 20 Nov 2015 21:53:08 -0800 Subject: [PATCH 41/78] feat(rewrite) waiting for schema consensus on schema_change --- spec/integration/cassandra_spec.lua | 10 +----- spec/integration/cql_types_spec.lua | 2 -- spec/schema.lua | 20 +++++++++++ spec/spec_utils.lua | 7 ---- src/cassandra.lua | 53 +++++++++++++++++++++++++++-- src/cassandra/options.lua | 3 +- src/cassandra/utils/time.lua | 18 +++++++++- t/01-cassandra.t | 45 ++++++++++++++++++++++++ 8 files changed, 135 insertions(+), 23 deletions(-) create mode 100644 spec/schema.lua diff --git a/spec/integration/cassandra_spec.lua b/spec/integration/cassandra_spec.lua index 8de9e95..f27390d 100644 --- a/spec/integration/cassandra_spec.lua +++ b/spec/integration/cassandra_spec.lua @@ -119,7 +119,7 @@ describe("spawn session", function() assert.equal(1, #rows) assert.equal("local", rows[1].key) end) - it("should parse SCHEMA_CHANGE/SET_KEYSPACE results", function() + it("should parse SCHEMA_CHANGE/SET_KEYSPACE results and wait for schema consensus", function() local res, err = session:execute [[ CREATE KEYSPACE IF NOT EXISTS resty_cassandra_spec_parsing WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor': 1} @@ -132,8 +132,6 @@ describe("spawn session", function() assert.equal("KEYSPACE", res.keyspace) assert.equal("resty_cassandra_spec_parsing", res.table) - utils.wait() - res, err = session:execute [[USE "resty_cassandra_spec_parsing"]] assert.falsy(err) assert.truthy(res) @@ -159,8 +157,6 @@ describe("spawn session", function() ]] assert.falsy(err) - utils.wait() - local rows, err = session_in_keyspace:execute("SELECT * FROM users") assert.falsy(err) assert.truthy(rows) @@ -196,8 +192,6 @@ describe("session", function() ) ]] assert.falsy(err) - - utils.wait() end) teardown(function() @@ -407,8 +401,6 @@ describe("session", function() ) ]] assert.falsy(err) - - utils.wait() end) it("should execute logged batched queries with no params", function() diff --git a/spec/integration/cql_types_spec.lua b/spec/integration/cql_types_spec.lua index 986b5e1..5f2b5a9 100644 --- a/spec/integration/cql_types_spec.lua +++ b/spec/integration/cql_types_spec.lua @@ -63,8 +63,6 @@ describe("CQL types integration", function() ) ]] assert.falsy(err) - - utils.wait() end) teardown(function() diff --git a/spec/schema.lua b/spec/schema.lua new file mode 100644 index 0000000..fd26d78 --- /dev/null +++ b/spec/schema.lua @@ -0,0 +1,20 @@ +package.path = "src/?.lua;src/?/init.lua;"..package.path +local inspect = require "inspect" +local cassandra = require "cassandra" +local log = require "cassandra.log" + +log.set_lvl("ERR") + +local _, err = cassandra.spawn_cluster {shm = "cassandra", contact_points = {"127.0.0.1", "127.0.0.2"}} +assert(err == nil, inspect(err)) + +local session, err = cassandra.spawn_session {shm = "cassandra", keyspace = "page"} +assert(err == nil, inspect(err)) + +local res, err = session:execute [[ + CREATE KEYSPACE IF NOT EXISTS stuff + WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor': 1} +]] +if err then + error(err) +end diff --git a/spec/spec_utils.lua b/spec/spec_utils.lua index f305a90..3c41301 100644 --- a/spec/spec_utils.lua +++ b/spec/spec_utils.lua @@ -5,11 +5,6 @@ local assert = require "luassert.assert" local _M = {} -function _M.wait(t) - if not t then t = 1 end - os.execute("sleep "..t) -end - function _M.set_log_lvl(lvl) log.set_lvl(lvl) end @@ -23,8 +18,6 @@ function _M.create_keyspace(session, keyspace) error(err) end - _M.wait() - return res end diff --git a/src/cassandra.lua b/src/cassandra.lua index 7979fff..92c1f21 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -1,7 +1,5 @@ -- @TODO -- option for max prepared queries in cache --- batches --- prepared batches? -- tracing -- wait for schema consensus on SCHEMA_CHANGE results -- @@ -372,6 +370,48 @@ function RequestHandler:get_next_coordinator() return nil, Errors.NoHostAvailableError(errors) end +local function check_schema_consensus(request_handler) + if #request_handler.hosts == 1 then + return true + end + + local local_query = Requests.QueryRequest("SELECT schema_version FROM system.local") + local local_res, err = request_handler.coordinator:send(local_query) + if err then + return nil, err + end + + local peers_query = Requests.QueryRequest("SELECT schema_version FROM system.peers") + local peers_res, err = request_handler.coordinator:send(peers_query) + if err then + return nil, err + end + + local match = true + for _, peer_row in ipairs(peers_res) do + if peer_row.schema_version ~= local_res[1].schema_version then + match = false + break + end + end + + return match +end + +function RequestHandler:wait_for_schema_consensus() + log.info("Waiting for schema consensus") + + local match, err + local start = time_utils.get_time() + + repeat + time_utils.wait(0.5) + match, err = check_schema_consensus(self) + until match or err ~= nil or (time_utils.get_time() - start) < self.options.protocol_options.max_schema_consensus_wait + + return err +end + function RequestHandler:send_on_next_coordinator(request) local coordinator, err = self:get_next_coordinator() if err then @@ -391,7 +431,7 @@ function RequestHandler:send(request) local result, err = self.coordinator:send(request) if self.coordinator.socket_type == "ngx" then - self.coordinator:set_keep_alive() + --self.coordinator:set_keep_alive() end if err then @@ -404,6 +444,13 @@ function RequestHandler:send(request) return nil, cache_err end + if result.type == "SCHEMA_CHANGE" then + local err = self:wait_for_schema_consensus() + if err then + log.warn("There was an error while waiting for the schema consensus between nodes: "..err) + end + end + return result end diff --git a/src/cassandra/options.lua b/src/cassandra/options.lua index 1132788..e882939 100644 --- a/src/cassandra/options.lua +++ b/src/cassandra/options.lua @@ -24,7 +24,8 @@ local DEFAULTS = { retry_on_timeout = true }, protocol_options = { - default_port = 9042 + default_port = 9042, + max_schema_consensus_wait = 5000 }, socket_options = { connect_timeout = 1000, diff --git a/src/cassandra/utils/time.lua b/src/cassandra/utils/time.lua index 3500861..33cd225 100644 --- a/src/cassandra/utils/time.lua +++ b/src/cassandra/utils/time.lua @@ -1,4 +1,10 @@ local type = type +local exec = os.execute +local ngx_sleep +local is_ngx = ngx ~= nil +if is_ngx then + ngx_sleep = ngx.sleep +end local function get_time() if ngx and type(ngx.now) == "function" then @@ -8,6 +14,16 @@ local function get_time() end end +local function wait(t) + if t == nil then t = 0.5 end + if is_ngx then + ngx_sleep(t) + else + exec("sleep "..t) + end +end + return { - get_time = get_time + get_time = get_time, + wait = wait } diff --git a/t/01-cassandra.t b/t/01-cassandra.t index 9e5f820..963f297 100644 --- a/t/01-cassandra.t +++ b/t/01-cassandra.t @@ -129,3 +129,48 @@ type: ROWS local --- no_error_log [error] + + + +=== TEST 5: wait for schema consensus +--- http_config eval +"$::HttpConfig + $::SpawnCluster" +--- config + location /t { + content_by_lua ' + local cassandra = require "cassandra" + local session = cassandra.spawn_session {shm = "cassandra"} + local res, err = session:execute [[ + CREATE KEYSPACE IF NOT EXISTS resty_t_keyspace + WITH REPLICATION = {\'class\': \'SimpleStrategy\', \'replication_factor\': 1} + ]] + if err then + ngx.log(ngx.ERR, tostring(err)) + ngx.exit(500) + end + + res, err = session:execute [[ + CREATE TABLE IF NOT EXISTS resty_t_keyspace.users( + id uuid PRIMARY KEY, + name text + ) + ]] + if err then + ngx.log(ngx.ERR, tostring(err)) + ngx.exit(500) + end + + res, err = session:execute("DROP KEYSPACE resty_t_keyspace") + if err then + ngx.log(ngx.ERR, tostring(err)) + ngx.exit(500) + end + '; + } +--- request +GET /t +--- response_body + +--- no_error_log +[error] From 4dfd4e3fee48203a0e6a898ad579c24114221136 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Sat, 21 Nov 2015 20:57:09 -0800 Subject: [PATCH 42/78] feat(rewrite) session:shutdown() and session:set_keep_alive() --- spec/integration/cassandra_spec.lua | 12 +++-- src/cassandra.lua | 32 +++++++----- t/01-cassandra.t | 76 +++++++++++++++++++++++++++-- 3 files changed, 103 insertions(+), 17 deletions(-) diff --git a/spec/integration/cassandra_spec.lua b/spec/integration/cassandra_spec.lua index f27390d..d768ddf 100644 --- a/spec/integration/cassandra_spec.lua +++ b/spec/integration/cassandra_spec.lua @@ -537,8 +537,14 @@ describe("session", function() end) describe(":shutdown()", function() - session:shutdown() - assert.True(session.terminated) - assert.same({}, session.hosts) + it("should close all connection and make the session unusable", function() + session:shutdown() + assert.True(session.terminated) + assert.same({}, session.hosts) + local rows, err = session:execute("SELECT * FROM users") + assert.truthy(err) + assert.equal("NoHostAvailableError", err.type) + assert.falsy(rows) + end) end) end) diff --git a/src/cassandra.lua b/src/cassandra.lua index 92c1f21..2562750 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -1,7 +1,6 @@ -- @TODO -- option for max prepared queries in cache -- tracing --- wait for schema consensus on SCHEMA_CHANGE results -- -- better logging -- more options validation @@ -232,10 +231,15 @@ function Host:get_reused_times() end function Host:set_keep_alive() + -- don't close if the connection was not opened yet + if not self.connected then + return true + end + if self.socket_type == "ngx" then local ok, err = self.socket:setkeepalive() if err then - log.err("Could not set keepalive for socket to "..self.address..". "..err) + log.err("Could not set keepalive socket to "..self.address..". "..err) return ok, err end end @@ -245,15 +249,20 @@ function Host:set_keep_alive() end function Host:close() + -- don't close if the connection was not opened yet + if not self.connected then + return true + end + log.info("Closing connection to "..self.address..".") local res, err = self.socket:close() if res ~= 1 then - log.err("Could not close socket for connection to "..self.address..". "..err) + log.err("Could not close socket to "..self.address..". "..err) return false, err - else - self.connected = false - return true end + + self.connected = false + return true end function Host:set_down() @@ -429,11 +438,6 @@ function RequestHandler:send(request) end local result, err = self.coordinator:send(request) - - if self.coordinator.socket_type == "ngx" then - --self.coordinator:set_keep_alive() - end - if err then return self:handle_error(request, err) end @@ -671,6 +675,12 @@ function Session:set_keyspace(keyspace) return true end +function Session:set_keep_alive() + for _, host in ipairs(self.hosts) do + host:set_keep_alive() + end +end + function Session:shutdown() for _, host in ipairs(self.hosts) do host:close() diff --git a/t/01-cassandra.t b/t/01-cassandra.t index 963f297..ee74ed5 100644 --- a/t/01-cassandra.t +++ b/t/01-cassandra.t @@ -66,7 +66,7 @@ GET /t -=== TEST 3: session:execute() +=== TEST 2: session:execute() --- http_config eval "$::HttpConfig $::SpawnCluster" @@ -99,7 +99,7 @@ local -=== TEST 4: session:execute() with request arguments +=== TEST 3: session:execute() with request arguments --- http_config eval "$::HttpConfig $::SpawnCluster" @@ -132,7 +132,7 @@ local -=== TEST 5: wait for schema consensus +=== TEST 4: wait for schema consensus --- http_config eval "$::HttpConfig $::SpawnCluster" @@ -174,3 +174,73 @@ GET /t --- no_error_log [error] + + + +=== TEST 5: session:shutdown() +--- http_config eval +"$::HttpConfig + $::SpawnCluster" +--- config + location /t { + content_by_lua ' + local cassandra = require "cassandra" + local session = cassandra.spawn_session {shm = "cassandra"} + local rows, err = session:execute("SELECT key FROM system.local") + if err then + ngx.log(ngx.ERR, tostring(err)) + ngx.exit(500) + end + + session:shutdown() + + local rows, err = session:execute("SELECT key FROM system.local") + if err then + ngx.say(tostring(err)) + return ngx.exit(200) + end + + ngx.exit(500) + '; + } +--- request +GET /t +--- response_body +NoHostAvailableError: Cannot reuse a session that has been shut down. +--- no_error_log +[error] + + + +=== TEST 6: session:set_keep_alive() +--- http_config eval +"$::HttpConfig + $::SpawnCluster" +--- config + location /t { + content_by_lua ' + local cassandra = require "cassandra" + local session = cassandra.spawn_session {shm = "cassandra"} + local rows, err = session:execute("SELECT key FROM system.local") + if err then + ngx.log(ngx.ERR, tostring(err)) + ngx.exit(500) + end + + session:set_keep_alive() + + local rows, err = session:execute("SELECT key FROM system.local") + if err then + ngx.log(ngx.ERR, tostring(err)) + ngx.exit(500) + end + + ngx.exit(200) + '; + } +--- request +GET /t +--- response_body + +--- no_error_log +[error] From e0628ed20bb0d6c815cad386e69eaaadf8361adb Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Mon, 23 Nov 2015 15:39:32 -0800 Subject: [PATCH 43/78] feat(rewrite) use a separate shm for prepared statements --- src/cassandra.lua | 2 +- src/cassandra/cache.lua | 66 ++++++++++++++++++++++++++++++--------- src/cassandra/options.lua | 10 +++++- t/01-cassandra.t | 39 +++++++++++++++++++++++ 4 files changed, 101 insertions(+), 16 deletions(-) diff --git a/src/cassandra.lua b/src/cassandra.lua index 2562750..83b54ee 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -1,5 +1,5 @@ -- @TODO --- option for max prepared queries in cache +-- authentication -- tracing -- -- better logging diff --git a/src/cassandra/cache.lua b/src/cassandra/cache.lua index 4b656c1..2157bd7 100644 --- a/src/cassandra/cache.lua +++ b/src/cassandra/cache.lua @@ -9,25 +9,34 @@ local shared local SharedDict = {} +local function set(data, key, value) + data[key] = { + value = value, + info = {expired = false} + } +end + function SharedDict:new() return setmetatable({data = {}}, {__index = self}) end function SharedDict:get(key) - return self.data[key], nil + return self.data[key] and self.data[key].value, nil end function SharedDict:set(key, value) - self.data[key] = value + set(self.data, key, value) return true, nil, false end +SharedDict.safe_set = SharedDict.set + function SharedDict:add(key, value) if self.data[key] ~= nil then return false, "exists", false end - self.data[key] = value + set(self.data, key, value) return true, nil, false end @@ -36,7 +45,7 @@ function SharedDict:replace(key, value) return false, "not found", false end - self.data[key] = value + set(self.data, key, value) return true, nil, false end @@ -47,12 +56,37 @@ end function SharedDict:incr(key, value) if not self.data[key] then return nil, "not found" - elseif type(self.data[key]) ~= "number" then + elseif type(self.data[key].value) ~= "number" then return nil, "not a number" end - self.data[key] = self.data[key] + value - return self.data[key], nil + self.data[key].value = self.data[key].value + value + return self.data[key].value, nil +end + +function SharedDict:flush_all() + for _, item in pairs(self.data) do + item.info.expired = true + end +end + +function SharedDict:flush_expired(n) + local data = self.data + local flushed = 0 + + for key, item in pairs(self.data) do + if item.info.expired then + data[key] = nil + flushed = flushed + 1 + if n and flushed == n then + break + end + end + end + + self.data = data + + return flushed end if in_ngx then @@ -79,7 +113,7 @@ local _SEP = ";" local function set_hosts(shm, hosts) local dict = get_dict(shm) - local ok, err = dict:set(_HOSTS_KEY, table_concat(hosts, _SEP)) + local ok, err = dict:safe_set(_HOSTS_KEY, table_concat(hosts, _SEP)) if not ok then err = "Cannot store hosts for cluster under shm "..shm..": "..err end @@ -103,7 +137,7 @@ end local function set_host(shm, host_addr, host) local dict = get_dict(shm) - local ok, err = dict:set(host_addr, json.encode(host)) + local ok, err = dict:safe_set(host_addr, json.encode(host)) if not ok then err = "Cannot store host details for cluster "..shm..": "..err end @@ -129,24 +163,28 @@ local function key_for_prepared_query(keyspace, query) end local function set_prepared_query_id(options, query, query_id) - local shm = options.shm + local shm = options.prepared_shm local dict = get_dict(shm) local prepared_key = key_for_prepared_query(options.keyspace, query) - local ok, err = dict:set(prepared_key, query_id) + local ok, err, forcible = dict:set(prepared_key, query_id) if not ok then - err = "Cannot store prepared query id for cluster "..shm..": "..err + err = "Cannot store prepared query id in shm "..shm..": "..err + elseif forcible then + log.warn("Prepared shm "..shm.." running out of memory. Consider increasing its size.") + dict:flush_expired(1) end return ok, err end local function get_prepared_query_id(options, query) - local dict = get_dict(options.shm) + local shm = options.prepared_shm + local dict = get_dict(shm) local prepared_key = key_for_prepared_query(options.keyspace, query) local value, err = dict:get(prepared_key) if err then - err = "Cannot retrieve prepared query id for cluster "..options.shm..": "..err + err = "Cannot retrieve prepared query id in shm "..shm..": "..err end return value, err end diff --git a/src/cassandra/options.lua b/src/cassandra/options.lua index e882939..acb97e1 100644 --- a/src/cassandra/options.lua +++ b/src/cassandra/options.lua @@ -6,8 +6,9 @@ local utils = require "cassandra.utils.table" local DEFAULTS = { shm = nil, -- stub + prepared_shm = nil, -- stub contact_points = {}, - keyspace = nil, -- stub + keyspace = nil, -- stub, policies = { address_resolution = require "cassandra.policies.address_resolution", load_balancing = require("cassandra.policies.load_balancing").SharedRoundRobin, @@ -45,6 +46,13 @@ local function parse_session(options) assert(type(options.shm) == "string", "shm must be a string") assert(options.shm ~= "", "shm must be a valid string") + if options.prepared_shm == nil then + options.prepared_shm = options.shm + end + + assert(type(options.prepared_shm) == "string", "prepared_shm must be a string") + assert(options.prepared_shm ~= "", "prepared_shm must be a valid string") + assert(type(options.protocol_options.default_port) == "number", "protocol default_port must be a number") assert(type(options.policies.address_resolution) == "function", "address_resolution policy must be a function") diff --git a/t/01-cassandra.t b/t/01-cassandra.t index ee74ed5..ade2040 100644 --- a/t/01-cassandra.t +++ b/t/01-cassandra.t @@ -13,6 +13,7 @@ _EOC_ our $SpawnCluster = <<_EOC_; lua_shared_dict cassandra 1m; + lua_shared_dict cassandra_prepared 1m; init_by_lua ' local cassandra = require "cassandra" local ok, err = cassandra.spawn_cluster({ @@ -244,3 +245,41 @@ GET /t --- no_error_log [error] + + + +=== TEST 7: session:execute() prepared query +--- http_config eval +"$::HttpConfig + $::SpawnCluster" +--- config + location /t { + content_by_lua ' + local cassandra = require "cassandra" + local session = cassandra.spawn_session {shm = "cassandra", prepared_shm = "cassandra_prepared"} + + for i = 1, 10 do + local rows, err = session:execute("SELECT key FROM system.local", nil, {prepare = true}) + if err then + ngx.log(ngx.ERR, tostring(err)) + ngx.exit(500) + end + ngx.say(rows[1].key) + end + '; + } +--- request +GET /t +--- response_body +local +local +local +local +local +local +local +local +local +local +--- no_error_log +[error] From e6dffd8d9ea77f9742e3f1fc401fcd63445cf6d1 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Mon, 23 Nov 2015 19:58:49 -0800 Subject: [PATCH 44/78] feat(rewrite) SSL verify + client auth for LuaSocket --- spec/ssl.lua | 27 ++++++++++++++++++++++ src/cassandra.lua | 48 +++++++++++++++++++++++++++++++++++++-- src/cassandra/options.lua | 10 ++++++-- 3 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 spec/ssl.lua diff --git a/spec/ssl.lua b/spec/ssl.lua new file mode 100644 index 0000000..ce8ae71 --- /dev/null +++ b/spec/ssl.lua @@ -0,0 +1,27 @@ +package.path = "src/?.lua;src/?/init.lua;"..package.path +local inspect = require "inspect" +local cassandra = require "cassandra" +local log = require "cassandra.log" + +log.set_lvl("INFO") + +local ssl_options = { + ca = "/Users/thibaultcha/.ccm/sslverify/client.cer.pem", + certificate = "/Users/thibaultcha/.ccm/sslverify/client.pem", + key = "/Users/thibaultcha/.ccm/sslverify/client.key", + verify = true +} + +local _, err = cassandra.spawn_cluster {shm = "cassandra", contact_points = {"127.0.0.1:9042"}, ssl_options = ssl_options} +assert(err == nil, inspect(err)) + +local session, err = cassandra.spawn_session {shm = "cassandra", ssl_options = ssl_options} +assert(err == nil, inspect(err)) + +local res, err = session:execute("SELECT peer FROM system.peers") +if err then + error(err) +end + +local inspect = require "inspect" +print(inspect(res)) diff --git a/src/cassandra.lua b/src/cassandra.lua index 83b54ee..1f33f36 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -141,6 +141,43 @@ local function change_keyspace(self, keyspace) return self:send(keyspace_req) end +local function do_ssl_handshake(self) + local ssl_options = self.options.ssl_options + + if self.socket_type == "luasocket" then + local ok, res = pcall(require, "ssl") + if not ok and string_find(res, "module 'ssl' not found", nil, true) then + error("LuaSec not found. Please install LuaSec to use SSL with LuaSocket.") + end + local ssl = res + local params = { + mode = "client", + protocol = "tlsv1", + key = ssl_options.key, + certificate = ssl_options.certificate, + cafile = ssl_options.ca, + verify = ssl_options.verify and "peer" or "none", + options = "all" + } + + local err + self.socket, err = ssl.wrap(self.socket, params) + if err then + return false, err + end + + ok, err = self.socket:dohandshake() + if err then + return false, err + end + else + -- returns a boolean since`reused_session` is false. + return self.socket:sslhandshake(false, nil, self.options.ssl_options.verify) + end + + return true +end + function Host:connect() if self.connected then return true end @@ -154,6 +191,13 @@ function Host:connect() return false, err, true end + if self.options.ssl_options ~= nil then + ok, err = do_ssl_handshake(self) + if not ok then + return false, err + end + end + log.info("Session connected to "..self.address) if self:get_reused_times() > 0 then @@ -255,8 +299,8 @@ function Host:close() end log.info("Closing connection to "..self.address..".") - local res, err = self.socket:close() - if res ~= 1 then + local _, err = self.socket:close() + if err then log.err("Could not close socket to "..self.address..". "..err) return false, err end diff --git a/src/cassandra/options.lua b/src/cassandra/options.lua index acb97e1..42cf0b8 100644 --- a/src/cassandra/options.lua +++ b/src/cassandra/options.lua @@ -8,7 +8,7 @@ local DEFAULTS = { shm = nil, -- stub prepared_shm = nil, -- stub contact_points = {}, - keyspace = nil, -- stub, + keyspace = nil, -- stub policies = { address_resolution = require "cassandra.policies.address_resolution", load_balancing = require("cassandra.policies.load_balancing").SharedRoundRobin, @@ -31,7 +31,13 @@ local DEFAULTS = { socket_options = { connect_timeout = 1000, read_timeout = 2000 - } + }, + -- ssl_options = { + -- key = nil, + -- certificate = nil, + -- ca = nil, -- stub + -- verify = false + -- } } local function parse_session(options) From 7e6e72d1a77591b42e18769a87aa5a75bf5be066 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Mon, 23 Nov 2015 23:00:49 -0800 Subject: [PATCH 45/78] feat(rewrite) authentication (plain text password) --- spec/auth.lua | 38 ++++++++++++++++++++ src/cassandra.lua | 41 +++++++++++++++++++--- src/cassandra/auth/init.lua | 20 +++++++++++ src/cassandra/auth/plain_text_password.lua | 20 +++++++++++ src/cassandra/errors.lua | 3 ++ src/cassandra/frame_reader.lua | 34 ++++++++++++------ src/cassandra/options.lua | 11 +++--- src/cassandra/requests.lua | 17 ++++++++- 8 files changed, 165 insertions(+), 19 deletions(-) create mode 100644 spec/auth.lua create mode 100644 src/cassandra/auth/init.lua create mode 100644 src/cassandra/auth/plain_text_password.lua diff --git a/spec/auth.lua b/spec/auth.lua new file mode 100644 index 0000000..f0becb9 --- /dev/null +++ b/spec/auth.lua @@ -0,0 +1,38 @@ +package.path = "src/?.lua;src/?/init.lua;"..package.path +local inspect = require "inspect" +local cassandra = require "cassandra" +local log = require "cassandra.log" + +log.set_lvl("INFO") + +local ssl_options = { + ca = "/Users/thibaultcha/.ccm/sslverify/client.cer.pem", + --certificate = "/Users/thibaultcha/.ccm/sslverify/client.pem", + --key = "/Users/thibaultcha/.ccm/sslverify/client.key", + verify = true +} + +local _, err = cassandra.spawn_cluster { + shm = "cassandra", + contact_points = {"127.0.0.1:9042"}, + ssl_options = ssl_options, + username = "cassandra", + password = "cassandra" +} +assert(err == nil, inspect(err)) + +local session, err = cassandra.spawn_session { + shm = "cassandra", + ssl_options = ssl_options, + username = "cassandra", + password = "cassandra" +} +assert(err == nil, inspect(err)) + +local res, err = session:execute("SELECT peer FROM system.peers") +if err then + error(err) +end + +local inspect = require "inspect" +print(inspect(res)) diff --git a/src/cassandra.lua b/src/cassandra.lua index 1f33f36..6fdcafa 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -1,5 +1,5 @@ -- @TODO --- authentication +-- flush from dict on shutdown -- tracing -- -- better logging @@ -8,6 +8,7 @@ local log = require "cassandra.log" local opts = require "cassandra.options" +local auth = require "cassandra.auth" local types = require "cassandra.types" local cache = require "cassandra.cache" local Object = require "cassandra.classic" @@ -20,11 +21,11 @@ local string_utils = require "cassandra.utils.string" local FrameHeader = require "cassandra.types.frame_header" local FrameReader = require "cassandra.frame_reader" -local setmetatable = setmetatable -local table_insert = table.insert +local CQL_Errors = types.ERRORS local string_find = string.find +local table_insert = table.insert local string_format = string.format -local CQL_Errors = types.ERRORS +local setmetatable = setmetatable --- Host -- A connection to a single host. @@ -178,6 +179,21 @@ local function do_ssl_handshake(self) return true end +local function send_auth(self, authenticator) + local token = authenticator:initial_response() + local auth_request = Requests.AuthResponse(token) + local res, err = self:send(auth_request) + if err then + return nil, err + elseif res and res.authenticated then + return true + end + + -- For other authenticators: + -- evaluate challenge + -- on authenticate success +end + function Host:connect() if self.connected then return true end @@ -206,6 +222,7 @@ function Host:connect() end -- Startup request on first connection + local ready = false local res, err = startup(self) if err then log.info("Startup request failed. "..err) @@ -224,7 +241,23 @@ function Host:connect() end return false, err + elseif res.must_authenticate then + log.info("Host at "..self.address.." required authentication") + local authenticator, err = auth.new_authenticator(res.class_name, self.options) + if err then + return nil, Errors.AuthenticationError(err) + end + local ok, err = send_auth(self, authenticator) + if err then + return nil, Errors.AuthenticationError(err) + elseif ok then + ready = true + end elseif res.ready then + ready = true + end + + if ready then log.info("Host at "..self.address.." is ready with protocol v"..self.protocol_version) if self.options.keyspace ~= nil then diff --git a/src/cassandra/auth/init.lua b/src/cassandra/auth/init.lua new file mode 100644 index 0000000..2c933f6 --- /dev/null +++ b/src/cassandra/auth/init.lua @@ -0,0 +1,20 @@ +local AUTHENTICATORS = { + ["org.apache.cassandra.auth.PasswordAuthenticator"] = "cassandra.auth.plain_text_password" +} + +local function new_authenticator(class_name, options) + local auth_module = AUTHENTICATORS[class_name] + if auth_module == nil then + return nil, "No authenticator implemented for class "..class_name + end + + local authenticator = require(auth_module) + authenticator.__index = authenticator + setmetatable({}, authenticator) + local err = authenticator:new(options) + return authenticator, err +end + +return { + new_authenticator = new_authenticator +} diff --git a/src/cassandra/auth/plain_text_password.lua b/src/cassandra/auth/plain_text_password.lua new file mode 100644 index 0000000..1f1deac --- /dev/null +++ b/src/cassandra/auth/plain_text_password.lua @@ -0,0 +1,20 @@ +local string_format = string.format + +local PasswordAuthenticator = {} + +function PasswordAuthenticator:new(options) + if options.username == nil then + return "No username defined in options" + elseif options.password == nil then + return "No password defined in options" + end + + self.username = options.username + self.password = options.password +end + +function PasswordAuthenticator:initial_response() + return string_format("\0%s\0%s", self.username, self.password) +end + +return PasswordAuthenticator diff --git a/src/cassandra/errors.lua b/src/cassandra/errors.lua index 398c477..92fe09b 100644 --- a/src/cassandra/errors.lua +++ b/src/cassandra/errors.lua @@ -40,6 +40,9 @@ local ERROR_TYPES = { message = function(address) return "timeout for peer "..address end + }, + AuthenticationError = { + info = "Represents an authentication error from the driver or from a Cassandra node." } } diff --git a/src/cassandra/frame_reader.lua b/src/cassandra/frame_reader.lua index e56048e..4dc1e51 100644 --- a/src/cassandra/frame_reader.lua +++ b/src/cassandra/frame_reader.lua @@ -126,12 +126,12 @@ local FrameReader = Object:extend() function FrameReader:new(frameHeader, body_bytes) self.frameHeader = frameHeader - self.frameBody = Buffer(frameHeader.version, body_bytes) + self.frame_body = Buffer(frameHeader.version, body_bytes) end -local function parse_error(frameBody) - local code = frameBody:read_int() - local message = frameBody:read_string() +local function parse_error(frame_body) + local code = frame_body:read_int() + local message = frame_body:read_string() local code_translation = types.ERRORS_TRANSLATIONS[code] return errors.ResponseError(code, code_translation, message) end @@ -140,10 +140,20 @@ local function parse_ready() return {ready = true} end -local function parse_result(frameBody) - local result_kind = frameBody:read_int() +local function parse_result(frame_body) + local result_kind = frame_body:read_int() local parser = RESULT_PARSERS[result_kind] - return parser(frameBody) + return parser(frame_body) +end + +local function parse_authenticate(frame_body) + local class_name = frame_body:read_string() + return {must_authenticate = true, class_name = class_name} +end + +local function parse_auth_success(frame_body) + local token = frame_body:read_bytes() + return {authenticated = true, token = token} end --- Decode a response frame @@ -155,11 +165,15 @@ function FrameReader:parse() -- Parse frame depending on op_code if op_code == OP_CODES.ERROR then - return nil, parse_error(self.frameBody) + return nil, parse_error(self.frame_body) elseif op_code == OP_CODES.READY then - return parse_ready(self.frameBody) + return parse_ready(self.frame_body) + elseif op_code == OP_CODES.AUTHENTICATE then + return parse_authenticate(self.frame_body) + elseif op_code == OP_CODES.AUTH_SUCCESS then + return parse_auth_success(self.frame_body) elseif op_code == OP_CODES.RESULT then - return parse_result(self.frameBody) + return parse_result(self.frame_body) end end diff --git a/src/cassandra/options.lua b/src/cassandra/options.lua index 42cf0b8..72ebae1 100644 --- a/src/cassandra/options.lua +++ b/src/cassandra/options.lua @@ -4,11 +4,12 @@ local utils = require "cassandra.utils.table" --- Defaults -- @section defaults +-- Nil values are stubs for the sole purpose of documenting their availability. local DEFAULTS = { - shm = nil, -- stub - prepared_shm = nil, -- stub + shm = nil, + prepared_shm = nil, contact_points = {}, - keyspace = nil, -- stub + keyspace = nil, policies = { address_resolution = require "cassandra.policies.address_resolution", load_balancing = require("cassandra.policies.load_balancing").SharedRoundRobin, @@ -19,7 +20,7 @@ local DEFAULTS = { consistency = types.consistencies.one, serial_consistency = types.consistencies.serial, page_size = 5000, - paging_state = nil, -- stub + paging_state = nil, auto_paging = false, prepare = false, retry_on_timeout = true @@ -32,6 +33,8 @@ local DEFAULTS = { connect_timeout = 1000, read_timeout = 2000 }, + username = nil, + password = nil, -- ssl_options = { -- key = nil, -- certificate = nil, diff --git a/src/cassandra/requests.lua b/src/cassandra/requests.lua index 47665cc..72805f7 100644 --- a/src/cassandra/requests.lua +++ b/src/cassandra/requests.lua @@ -237,11 +237,26 @@ function BatchRequest:build() end end +--- AuthResponse +-- @section auth_response + +local AuthResponse = Request:extend() + +function AuthResponse:new(token) + self.token = token + AuthResponse.super.new(self, OP_CODES.AUTH_RESPONSE) +end + +function AuthResponse:build() + self.frame_body:write_bytes(self.token) +end + return { QueryRequest = QueryRequest, StartupRequest = StartupRequest, PrepareRequest = PrepareRequest, KeyspaceRequest = KeyspaceRequest, ExecutePreparedRequest = ExecutePreparedRequest, - BatchRequest = BatchRequest + BatchRequest = BatchRequest, + AuthResponse = AuthResponse } From aec9fe34fda7c49e5bd620db24660dc654a0efe9 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Mon, 23 Nov 2015 23:12:49 -0800 Subject: [PATCH 46/78] cleanup(rewrite) buffer and classic reorganization --- src/cassandra.lua | 29 +++++++++++-------- src/cassandra/{buffer.lua => buffer/init.lua} | 4 +-- .../buffer.lua => buffer/raw_buffer.lua} | 2 +- src/cassandra/frame_reader.lua | 2 +- src/cassandra/requests.lua | 2 +- src/cassandra/{ => utils}/classic.lua | 0 6 files changed, 22 insertions(+), 17 deletions(-) rename src/cassandra/{buffer.lua => buffer/init.lua} (98%) rename src/cassandra/{utils/buffer.lua => buffer/raw_buffer.lua} (94%) rename src/cassandra/{ => utils}/classic.lua (100%) diff --git a/src/cassandra.lua b/src/cassandra.lua index 6fdcafa..6b3a9af 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -11,7 +11,6 @@ local opts = require "cassandra.options" local auth = require "cassandra.auth" local types = require "cassandra.types" local cache = require "cassandra.cache" -local Object = require "cassandra.classic" local CONSTS = require "cassandra.constants" local Errors = require "cassandra.errors" local Requests = require "cassandra.requests" @@ -32,7 +31,8 @@ local setmetatable = setmetatable -- Not cluster aware, only maintain a socket to its peer. -- @section host -local Host = Object:extend() +local Host = {} +Host.__index = Host local function new_socket(self) local tcp_sock, sock_type @@ -60,15 +60,19 @@ function Host:new(address, options) local host, port = string_utils.split_by_colon(address) if not port then port = options.protocol_options.default_port end - self.host = host - self.port = port - self.address = address - self.protocol_version = CONSTS.DEFAULT_PROTOCOL_VERSION + local h = {} - self.options = options - self.reconnection_policy = self.options.policies.reconnection + h.host = host + h.port = port + h.address = address + h.protocol_version = CONSTS.DEFAULT_PROTOCOL_VERSION - new_socket(self) + h.options = options + h.reconnection_policy = h.options.policies.reconnection + + new_socket(h) + + return setmetatable(h, Host) end function Host:decrease_version() @@ -399,6 +403,7 @@ end -- @section request_handler local RequestHandler = {} +RequestHandler.__index = RequestHandler function RequestHandler:new(hosts, options) local o = { @@ -407,7 +412,7 @@ function RequestHandler:new(hosts, options) n_retries = 0 } - return setmetatable(o, {__index = self}) + return setmetatable(o, RequestHandler) end function RequestHandler.get_first_coordinator(hosts) @@ -624,7 +629,7 @@ function Session:new(options) end for _, addr in ipairs(host_addresses) do - table_insert(s.hosts, Host(addr, options)) + table_insert(s.hosts, Host:new(addr, options)) end return setmetatable(s, {__index = self}) @@ -851,7 +856,7 @@ function Cassandra.spawn_cluster(options) local contact_points_hosts = {} for _, contact_point in ipairs(options.contact_points) do - table_insert(contact_points_hosts, Host(contact_point, options)) + table_insert(contact_points_hosts, Host:new(contact_point, options)) end return Cassandra.refresh_hosts(contact_points_hosts, options) diff --git a/src/cassandra/buffer.lua b/src/cassandra/buffer/init.lua similarity index 98% rename from src/cassandra/buffer.lua rename to src/cassandra/buffer/init.lua index 6804fe7..2c8404e 100644 --- a/src/cassandra/buffer.lua +++ b/src/cassandra/buffer/init.lua @@ -1,6 +1,6 @@ -local Buffer = require "cassandra.utils.buffer" -local t_utils = require "cassandra.utils.table" +local Buffer = require "cassandra.buffer.raw_buffer" local types = require "cassandra.types" +local t_utils = require "cassandra.utils.table" local cql_types = types.cql_types local math_floor = math.floor diff --git a/src/cassandra/utils/buffer.lua b/src/cassandra/buffer/raw_buffer.lua similarity index 94% rename from src/cassandra/utils/buffer.lua rename to src/cassandra/buffer/raw_buffer.lua index 303a4d5..8f6f1e0 100644 --- a/src/cassandra/utils/buffer.lua +++ b/src/cassandra/buffer/raw_buffer.lua @@ -1,4 +1,4 @@ -local Object = require "cassandra.classic" +local Object = require "cassandra.utils.classic" local string_sub = string.sub local Buffer = Object:extend() diff --git a/src/cassandra/frame_reader.lua b/src/cassandra/frame_reader.lua index 4dc1e51..bf7ed3c 100644 --- a/src/cassandra/frame_reader.lua +++ b/src/cassandra/frame_reader.lua @@ -1,6 +1,6 @@ local bit = require "cassandra.utils.bit" local types = require "cassandra.types" -local Object = require "cassandra.classic" +local Object = require "cassandra.utils.classic" local Buffer = require "cassandra.buffer" local errors = require "cassandra.errors" local OP_CODES = types.OP_CODES diff --git a/src/cassandra/requests.lua b/src/cassandra/requests.lua index 72805f7..a802124 100644 --- a/src/cassandra/requests.lua +++ b/src/cassandra/requests.lua @@ -1,7 +1,7 @@ local bit = require "cassandra.utils.bit" local types = require "cassandra.types" local CONSTS = require "cassandra.constants" -local Object = require "cassandra.classic" +local Object = require "cassandra.utils.classic" local Buffer = require "cassandra.buffer" local FrameHeader = require "cassandra.types.frame_header" diff --git a/src/cassandra/classic.lua b/src/cassandra/utils/classic.lua similarity index 100% rename from src/cassandra/classic.lua rename to src/cassandra/utils/classic.lua From b0b7c6813160a38fe6cc846f7f47634df2da6963 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Tue, 24 Nov 2015 13:45:17 -0800 Subject: [PATCH 47/78] style(lint) make luacheck happy --- .luacheckrc | 2 +- spec/integration/cassandra_spec.lua | 11 ++++++----- spec/load.lua | 11 +++-------- spec/schema.lua | 2 +- spec/unit/utils_spec.lua | 2 +- src/cassandra.lua | 14 +++++++------- src/cassandra/cache.lua | 1 + src/cassandra/frame_reader.lua | 2 +- src/cassandra/types/frame_header.lua | 6 +++--- src/cassandra/utils/table.lua | 1 - 10 files changed, 24 insertions(+), 28 deletions(-) diff --git a/.luacheckrc b/.luacheckrc index 68c5912..ce069b7 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -1,3 +1,3 @@ unused_args = false redefined = false -globals = {"ngx", "describe", "setup", "teardown", "it", "pending", "after_each", "spy"} +globals = {"ngx", "describe", "setup", "teardown", "it", "pending", "after_each", "finally", "spy"} diff --git a/spec/integration/cassandra_spec.lua b/spec/integration/cassandra_spec.lua index d768ddf..39840c0 100644 --- a/spec/integration/cassandra_spec.lua +++ b/spec/integration/cassandra_spec.lua @@ -384,7 +384,8 @@ describe("session", function() page_tracker = page end - assert.spy(cache.get_prepared_query_id).was.called(page) + assert.equal(1000, page_tracker) + assert.spy(cache.get_prepared_query_id).was.called(page_tracker + 1) assert.spy(cache.set_prepared_query_id).was.called(0) end) end) @@ -468,7 +469,7 @@ describe("session", function() assert.equal(3, row.value) end) it("should return any error", function() - local res, err = session:batch({ + local _, err = session:batch({ {"INSERT WHATEVER"}, {"INSERT THING"} }) @@ -476,7 +477,7 @@ describe("session", function() assert.equal("ResponseError", err.type) end) it("should support protocol level timestamp", function() - local res, err = session:batch({ + local _, err = session:batch({ {"INSERT INTO users(id, name, n) VALUES(".._UUID..", 'Alice', 4)"}, {"UPDATE users SET name = 'Alice' WHERE id = ".._UUID.." AND n = 4"}, {"UPDATE users SET name = 'Alicia4' WHERE id = ".._UUID.." AND n = 4"} @@ -491,7 +492,7 @@ describe("session", function() assert.equal(1428311323417123, row["writetime(name)"]) end) it("should support serial consistency", function() - local res, err = session:batch({ + local _, err = session:batch({ {"INSERT INTO users(id, name, n) VALUES(".._UUID..", 'Alice', 5)"}, {"UPDATE users SET name = 'Alice' WHERE id = ".._UUID.." AND n = 5"}, {"UPDATE users SET name = 'Alicia5' WHERE id = ".._UUID.." AND n = 5"} @@ -513,7 +514,7 @@ describe("session", function() cache.set_prepared_query_id:revert() end) - local res, err = session:batch({ + local _, err = session:batch({ {"INSERT INTO users(id, name, n) VALUES(?, ?, ?)", {cassandra.uuid(_UUID), "Alice", 6}}, {"INSERT INTO users(id, name, n) VALUES(?, ?, ?)", {cassandra.uuid(_UUID), "Alice", 7}}, {"UPDATE users SET name = ? WHERE id = ? AND n = ?", {"Alicia", cassandra.uuid(_UUID), 6}}, diff --git a/spec/load.lua b/spec/load.lua index 17a462e..eb5cfaa 100644 --- a/spec/load.lua +++ b/spec/load.lua @@ -5,17 +5,12 @@ local log = require "cassandra.log" log.set_lvl("INFO") -local ok, err = cassandra.spawn_cluster { +local _, err = cassandra.spawn_cluster { shm = "cassandra", contact_points = {"127.0.0.1", "127.0.0.2"} } assert(err == nil, inspect(err)) - - - - - local session, err = cassandra.spawn_session { shm = "cassandra" } @@ -25,7 +20,7 @@ assert(err == nil, inspect(err)) --while true do --i = i + 1 for i = 1, 1000 do - local res, err = session:execute("SELECT peer FROM system.peers") + local _, err = session:execute("SELECT peer FROM system.peers") if err then print(inspect(err)) error() @@ -35,7 +30,7 @@ end session:shutdown() -local res, err = session:execute("SELECT peer FROM system.peers") +local _, err = session:execute("SELECT peer FROM system.peers") if err then print(inspect(err)) error() diff --git a/spec/schema.lua b/spec/schema.lua index fd26d78..4cb4676 100644 --- a/spec/schema.lua +++ b/spec/schema.lua @@ -11,7 +11,7 @@ assert(err == nil, inspect(err)) local session, err = cassandra.spawn_session {shm = "cassandra", keyspace = "page"} assert(err == nil, inspect(err)) -local res, err = session:execute [[ +local _, err = session:execute [[ CREATE KEYSPACE IF NOT EXISTS stuff WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor': 1} ]] diff --git a/spec/unit/utils_spec.lua b/spec/unit/utils_spec.lua index 6bc19e6..9bd151a 100644 --- a/spec/unit/utils_spec.lua +++ b/spec/unit/utils_spec.lua @@ -89,7 +89,7 @@ describe("table_utils", function() assert.equal("source", target.source) end) it("should not override nested properties in the target", function() - local source1 = {source1 = true, source_nested = {hello = "world"}} + local source = {source = true, source_nested = {hello = "world"}} local target = {target = true, source_nested = {hello = "universe"}} target = table_utils.extend_table(source, target) diff --git a/src/cassandra.lua b/src/cassandra.lua index 6b3a9af..4c9ab3a 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -100,18 +100,18 @@ local function send_and_receive(self, request) return nil, err end - local frameHeader = FrameHeader.from_raw_bytes(frame_version_byte, header_bytes) + local frame_header = FrameHeader.from_raw_bytes(frame_version_byte, header_bytes) -- Receive frame body local body_bytes - if frameHeader.body_length > 0 then - body_bytes, err = self.socket:receive(frameHeader.body_length) + if frame_header.body_length > 0 then + body_bytes, err = self.socket:receive(frame_header.body_length) if body_bytes == nil then return nil, err end end - return FrameReader(frameHeader, body_bytes) + return FrameReader(frame_header, body_bytes) end function Host:send(request) @@ -119,7 +119,7 @@ function Host:send(request) self:set_timeout(self.options.socket_options.read_timeout) - local frameReader, err = send_and_receive(self, request) + local frame_reader, err = send_and_receive(self, request) if err then if err == "timeout" then return nil, Errors.TimeoutError(self.address) @@ -129,7 +129,7 @@ function Host:send(request) end -- result, cql_error - return frameReader:parse() + return frame_reader:parse() end local function startup(self) @@ -171,7 +171,7 @@ local function do_ssl_handshake(self) return false, err end - ok, err = self.socket:dohandshake() + local _, err = self.socket:dohandshake() if err then return false, err end diff --git a/src/cassandra/cache.lua b/src/cassandra/cache.lua index 2157bd7..c5e3d06 100644 --- a/src/cassandra/cache.lua +++ b/src/cassandra/cache.lua @@ -1,3 +1,4 @@ +local log = require "cassandra.log" local json = require "cjson" local string_utils = require "cassandra.utils.string" local table_concat = table.concat diff --git a/src/cassandra/frame_reader.lua b/src/cassandra/frame_reader.lua index bf7ed3c..2bfe65c 100644 --- a/src/cassandra/frame_reader.lua +++ b/src/cassandra/frame_reader.lua @@ -33,7 +33,7 @@ local function parse_metadata(buffer) local has_more_pages = bit.btest(flags, ROWS_RESULT_FLAGS.HAS_MORE_PAGES) local has_global_table_spec = bit.btest(flags, ROWS_RESULT_FLAGS.GLOBAL_TABLES_SPEC) - local has_no_metadata = bit.btest(flags, ROWS_RESULT_FLAGS.NO_METADATA) + --local has_no_metadata = bit.btest(flags, ROWS_RESULT_FLAGS.NO_METADATA) if has_more_pages then paging_state = buffer:read_bytes() diff --git a/src/cassandra/types/frame_header.lua b/src/cassandra/types/frame_header.lua index 0391db1..a9fe6ed 100644 --- a/src/cassandra/types/frame_header.lua +++ b/src/cassandra/types/frame_header.lua @@ -23,10 +23,10 @@ setmetatable(VERSION_CODES, table_utils.const_mt) local FrameHeader = Buffer:extend() -function FrameHeader:new(version, flags, op_code, body_length) +function FrameHeader:new(version, flags, op_code, body_length, stream_id) self.flags = flags and flags or 0 self.op_code = op_code - self.stream_id = 0 + self.stream_id = stream_id or 0 self.body_length = body_length self.super.new(self, version) @@ -76,7 +76,7 @@ function FrameHeader.from_raw_bytes(version_byte, raw_bytes) local op_code = buffer:read_byte() local body_length = buffer:read_int() - return FrameHeader(version, flags, op_code, body_length) + return FrameHeader(version, flags, op_code, body_length, stream_id) end return FrameHeader diff --git a/src/cassandra/utils/table.lua b/src/cassandra/utils/table.lua index 7341da9..9eeedd9 100644 --- a/src/cassandra/utils/table.lua +++ b/src/cassandra/utils/table.lua @@ -5,7 +5,6 @@ local getmetatable = getmetatable local rawget = rawget local tostring = tostring local pairs = pairs -local unpack = unpack local type = type local _M = {} From 890a127fa3b9181f80e30100d207014dcbe9c070 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Tue, 24 Nov 2015 14:26:15 -0800 Subject: [PATCH 48/78] feat(rewrite) drop const_mt, constants and introduce cluster instance --- spec/integration/cassandra_spec.lua | 10 +++++++ spec/prepare.lua | 4 +-- spec/unit/cql_types_buffer_spec.lua | 5 ++-- spec/unit/utils_spec.lua | 44 ---------------------------- src/cassandra.lua | 35 +++++++++++++++++----- src/cassandra/constants.lua | 10 ------- src/cassandra/requests.lua | 7 +++-- src/cassandra/types/frame_header.lua | 5 +--- src/cassandra/utils/table.lua | 20 ------------- 9 files changed, 48 insertions(+), 92 deletions(-) delete mode 100644 src/cassandra/constants.lua diff --git a/spec/integration/cassandra_spec.lua b/spec/integration/cassandra_spec.lua index 39840c0..ba25a7c 100644 --- a/spec/integration/cassandra_spec.lua +++ b/spec/integration/cassandra_spec.lua @@ -85,6 +85,16 @@ describe("spawn cluster", function() assert.False(ok) assert.equal("NoHostAvailableError", err.type) end) + it("should return a third parameter, cluster, an instance able to spawn sessions", function() + local ok, err, cluster = cassandra.spawn_cluster({ + shm = "test", + contact_points = utils.contact_points + }) + assert.falsy(err) + assert.True(ok) + assert.truthy(cluster) + assert.truthy(cluster.spawn_session) + end) end) describe("spawn session", function() diff --git a/spec/prepare.lua b/spec/prepare.lua index de284db..46e22ee 100644 --- a/spec/prepare.lua +++ b/spec/prepare.lua @@ -5,10 +5,10 @@ local log = require "cassandra.log" log.set_lvl("ERR") -local _, err = cassandra.spawn_cluster {shm = "cassandra", contact_points = {"127.0.0.1", "127.0.0.2"}} +local _, err, cluster = cassandra.spawn_cluster {shm = "cassandra", contact_points = {"127.0.0.1", "127.0.0.2"}} assert(err == nil, inspect(err)) -local session, err = cassandra.spawn_session {shm = "cassandra", keyspace = "page"} +local session, err = cluster:spawn_session() assert(err == nil, inspect(err)) -- for i = 1, 10000 do diff --git a/spec/unit/cql_types_buffer_spec.lua b/spec/unit/cql_types_buffer_spec.lua index 97af681..080ff07 100644 --- a/spec/unit/cql_types_buffer_spec.lua +++ b/spec/unit/cql_types_buffer_spec.lua @@ -1,11 +1,12 @@ local utils = require "spec.spec_utils" local cassandra = require "cassandra" local Buffer = require "cassandra.buffer" -local CONSTS = require "cassandra.constants" local types = require "cassandra.types" local CQL_TYPES = types.cql_types -for _, protocol_version in ipairs(CONSTS.SUPPORTED_PROTOCOL_VERSIONS) do +SUPPORTED_PROTOCOL_VERSIONS = {cassandra.DEFAULT_PROTOCOL_VERSION, cassandra.MIN_PROTOCOL_VERSION} + +for _, protocol_version in ipairs(SUPPORTED_PROTOCOL_VERSIONS) do describe("CQL Types protocol v"..protocol_version, function() it("[uuid] should be bufferable", function() diff --git a/spec/unit/utils_spec.lua b/spec/unit/utils_spec.lua index 9bd151a..2c7d5e9 100644 --- a/spec/unit/utils_spec.lua +++ b/spec/unit/utils_spec.lua @@ -1,50 +1,6 @@ local table_utils = require "cassandra.utils.table" describe("table_utils", function() - describe("const_mt", function() - - local VERSION_CODES = { - [2] = { - REQUEST = 20, - RESPONSE = 21, - SOME_V2 = 2222 - }, - [3] = { - REQUEST = 30, - RESPONSE = 31, - SOME_V3_ONLY = 3333 - } - } - setmetatable(VERSION_CODES, table_utils.const_mt) - - local FLAGS = { - COMPRESSION = 1, - TRACING = 2, - [4] = { - CUSTOM_PAYLOAD = 4 - } - } - setmetatable(FLAGS, table_utils.const_mt) - - describe("#get()", function() - it("should get most recent version of a constant", function() - assert.equal(30, VERSION_CODES:get("REQUEST")) - assert.equal(31, VERSION_CODES:get("RESPONSE")) - assert.equal(3333, VERSION_CODES:get("SOME_V3_ONLY")) - assert.equal(2222, VERSION_CODES:get("SOME_V2")) - end) - it("should get constant from the root", function() - assert.equal(1, FLAGS:get("COMPRESSION")) - assert.equal(2, FLAGS:get("TRACING")) - end) - it("should accept a version parameter for which version to look into", function() - assert.equal(4, FLAGS:get("CUSTOM_PAYLOAD", 4)) - assert.equal(20, VERSION_CODES:get("REQUEST", 2)) - assert.equal(21, VERSION_CODES:get("RESPONSE", 2)) - end) - end) - - end) describe("extend_table", function() it("should extend a table from a source", function() local source = {source = true} diff --git a/src/cassandra.lua b/src/cassandra.lua index 4c9ab3a..f63fba4 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -11,7 +11,6 @@ local opts = require "cassandra.options" local auth = require "cassandra.auth" local types = require "cassandra.types" local cache = require "cassandra.cache" -local CONSTS = require "cassandra.constants" local Errors = require "cassandra.errors" local Requests = require "cassandra.requests" local time_utils = require "cassandra.utils.time" @@ -26,6 +25,12 @@ local table_insert = table.insert local string_format = string.format local setmetatable = setmetatable +--- Constants +-- @section constants + +local MIN_PROTOCOL_VERSION = 2 +local DEFAULT_PROTOCOL_VERSION = 3 + --- Host -- A connection to a single host. -- Not cluster aware, only maintain a socket to its peer. @@ -65,7 +70,7 @@ function Host:new(address, options) h.host = host h.port = port h.address = address - h.protocol_version = CONSTS.DEFAULT_PROTOCOL_VERSION + h.protocol_version = DEFAULT_PROTOCOL_VERSION h.options = options h.reconnection_policy = h.options.policies.reconnection @@ -235,7 +240,7 @@ function Host:connect() if string_find(err.message, "Invalid or unsupported protocol version:", nil, true) then self:close() self:decrease_version() - if self.protocol_version < CONSTS.MIN_PROTOCOL_VERSION then + if self.protocol_version < MIN_PROTOCOL_VERSION then log.err("Connection could not find a supported protocol version.") else log.info("Decreasing protocol version to v"..self.protocol_version) @@ -775,7 +780,9 @@ end -- @section cassandra local Cassandra = { - _VERSION = "0.4.0" + _VERSION = "0.4.0", + DEFAULT_PROTOCOL_VERSION = DEFAULT_PROTOCOL_VERSION, + MIN_PROTOCOL_VERSION = MIN_PROTOCOL_VERSION } function Cassandra.spawn_session(options) @@ -850,6 +857,13 @@ function Cassandra.refresh_hosts(contact_points_hosts, options) return cache.set_hosts(options.shm, addresses) end +local Cluster = {} +Cluster.__index = Cluster + +function Cluster:spawn_session() + return Cassandra.spawn_session(self.options) +end + --- Retrieve cluster informations and store them in ngx.shared.DICT function Cassandra.spawn_cluster(options) options = opts.parse_cluster(options) @@ -859,11 +873,18 @@ function Cassandra.spawn_cluster(options) table_insert(contact_points_hosts, Host:new(contact_point, options)) end - return Cassandra.refresh_hosts(contact_points_hosts, options) + local ok, err = Cassandra.refresh_hosts(contact_points_hosts, options) + if not ok then + return false, err + end + + return true, nil, setmetatable({ + options = options + }, Cluster) end ---- CQL types inferers --- @section +--- Cassandra Misc +-- @section cassandra_misc local CQL_TYPES = types.cql_types diff --git a/src/cassandra/constants.lua b/src/cassandra/constants.lua deleted file mode 100644 index cf4b517..0000000 --- a/src/cassandra/constants.lua +++ /dev/null @@ -1,10 +0,0 @@ -local SUPPORTED_PROTOCOL_VERSIONS = {2, 3} -local DEFAULT_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[#SUPPORTED_PROTOCOL_VERSIONS] - -return { - SUPPORTED_PROTOCOL_VERSIONS = SUPPORTED_PROTOCOL_VERSIONS, - DEFAULT_PROTOCOL_VERSION = DEFAULT_PROTOCOL_VERSION, - MIN_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[1], - MAX_PROTOCOL_VERSION = DEFAULT_PROTOCOL_VERSION, - CQL_VERSION = "3.0.0" -} diff --git a/src/cassandra/requests.lua b/src/cassandra/requests.lua index a802124..83ca708 100644 --- a/src/cassandra/requests.lua +++ b/src/cassandra/requests.lua @@ -1,13 +1,14 @@ local bit = require "cassandra.utils.bit" local types = require "cassandra.types" -local CONSTS = require "cassandra.constants" local Object = require "cassandra.utils.classic" local Buffer = require "cassandra.buffer" local FrameHeader = require "cassandra.types.frame_header" local OP_CODES = types.OP_CODES -local string_format = string.format local string_byte = string.byte +local string_format = string.format + +local CQL_VERSION = "3.0.0" --- Query Flags -- @section query_flags @@ -74,7 +75,7 @@ end function StartupRequest:build() self.frame_body:write_string_map({ - CQL_VERSION = CONSTS.CQL_VERSION + CQL_VERSION = CQL_VERSION }) end diff --git a/src/cassandra/types/frame_header.lua b/src/cassandra/types/frame_header.lua index a9fe6ed..4a03876 100644 --- a/src/cassandra/types/frame_header.lua +++ b/src/cassandra/types/frame_header.lua @@ -1,6 +1,5 @@ local bit = require "cassandra.utils.bit" local Buffer = require "cassandra.buffer" -local table_utils = require "cassandra.utils.table" --- CONST -- @section constants @@ -16,8 +15,6 @@ local VERSION_CODES = { } } -setmetatable(VERSION_CODES, table_utils.const_mt) - --- FrameHeader -- @section FrameHeader @@ -33,7 +30,7 @@ function FrameHeader:new(version, flags, op_code, body_length, stream_id) end function FrameHeader:dump() - FrameHeader.super.write_byte(self, VERSION_CODES:get("REQUEST", self.version)) + FrameHeader.super.write_byte(self, VERSION_CODES[self.version].REQUEST) FrameHeader.super.write_byte(self, self.flags) if self.version < 3 then diff --git a/src/cassandra/utils/table.lua b/src/cassandra/utils/table.lua index 9eeedd9..f897e68 100644 --- a/src/cassandra/utils/table.lua +++ b/src/cassandra/utils/table.lua @@ -1,5 +1,3 @@ -local CONSTS = require "cassandra.constants" - local setmetatable = setmetatable local getmetatable = getmetatable local rawget = rawget @@ -50,22 +48,4 @@ function _M.is_array(t) return true end -local _const_mt = { - get = function(t, key, version) - if not version then version = CONSTS.MAX_PROTOCOL_VERSION end - - local const, version_consts - while version >= CONSTS.MIN_PROTOCOL_VERSION and const == nil do - version_consts = t[version] ~= nil and t[version] or t - const = rawget(version_consts, key) - version = version - 1 - end - return const - end -} - -_const_mt.__index = _const_mt - -_M.const_mt = _const_mt - return _M From 530a5030a9e0090aba9c1e4350cc5ed4dd73ff10 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Tue, 24 Nov 2015 15:21:26 -0800 Subject: [PATCH 49/78] fix(retry) delay 1st retry and create a new socket --- spec/load.lua | 24 +++++++++++------------- spec/unit/cql_types_buffer_spec.lua | 2 +- src/cassandra.lua | 19 ++++++++++++++----- src/cassandra/policies/reconnection.lua | 4 ++-- src/cassandra/utils/table.lua | 1 - 5 files changed, 28 insertions(+), 22 deletions(-) diff --git a/spec/load.lua b/spec/load.lua index eb5cfaa..654ed90 100644 --- a/spec/load.lua +++ b/spec/load.lua @@ -16,22 +16,20 @@ local session, err = cassandra.spawn_session { } assert(err == nil, inspect(err)) ---local i = 0 ---while true do - --i = i + 1 -for i = 1, 1000 do +local i = 0 +while true do + i = i + 1 +--for i = 1, 1000 do local _, err = session:execute("SELECT peer FROM system.peers") if err then - print(inspect(err)) - error() + error(err) end - print("Request "..i.." successful.") + --print("Request "..i.." successful.") end -session:shutdown() +-- session:shutdown() -local _, err = session:execute("SELECT peer FROM system.peers") -if err then - print(inspect(err)) - error() -end +-- local _, err = session:execute("SELECT peer FROM system.peers") +-- if err then +-- error(err) +-- end diff --git a/spec/unit/cql_types_buffer_spec.lua b/spec/unit/cql_types_buffer_spec.lua index 080ff07..2e201bd 100644 --- a/spec/unit/cql_types_buffer_spec.lua +++ b/spec/unit/cql_types_buffer_spec.lua @@ -4,7 +4,7 @@ local Buffer = require "cassandra.buffer" local types = require "cassandra.types" local CQL_TYPES = types.cql_types -SUPPORTED_PROTOCOL_VERSIONS = {cassandra.DEFAULT_PROTOCOL_VERSION, cassandra.MIN_PROTOCOL_VERSION} +local SUPPORTED_PROTOCOL_VERSIONS = {cassandra.DEFAULT_PROTOCOL_VERSION, cassandra.MIN_PROTOCOL_VERSION} for _, protocol_version in ipairs(SUPPORTED_PROTOCOL_VERSIONS) do diff --git a/src/cassandra.lua b/src/cassandra.lua index f63fba4..31a6b7f 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -352,7 +352,7 @@ function Host:close() end function Host:set_down() - log.info("Setting host "..self.address.." as DOWN") + log.warn("Setting host "..self.address.." as DOWN") local host_infos, err = cache.get_host(self.options.shm, self.address) if err then return false, err @@ -360,6 +360,8 @@ function Host:set_down() host_infos.unhealthy_at = time_utils.get_time() host_infos.reconnection_delay = self.reconnection_policy.next(self) + self:close() + new_socket(self) return cache.set_host(self.options.shm, self.address, host_infos) end @@ -372,7 +374,7 @@ function Host:set_up() -- host was previously marked a DOWN if host_infos.unhealthy_at ~= 0 then - log.info("Setting host "..self.address.." as UP") + log.warn("Setting host "..self.address.." as UP") host_infos.unhealthy_at = 0 -- reset schedule for reconnection delay self.reconnection_policy.new_schedule(self) @@ -401,7 +403,14 @@ function Host:can_be_considered_up() return nil, err end - return is_up or (time_utils.get_time() - host_infos.unhealthy_at >= host_infos.reconnection_delay) + local delay = time_utils.get_time() - host_infos.unhealthy_at + + if is_up then + return true + elseif (delay >= host_infos.reconnection_delay) then + log.info("Host "..self.address.." could nbe considered up after "..delay.."ms") + return true + end end --- Request Handler @@ -514,7 +523,7 @@ function RequestHandler:send_on_next_coordinator(request) return nil, err end - log.info("Acquired connection through load balancing policy: "..coordinator.address) + log.debug("Acquired connection through load balancing policy: "..coordinator.address) return self:send(request) end @@ -591,7 +600,7 @@ end function RequestHandler:retry(request) self.n_retries = self.n_retries + 1 - log.info("Retrying request") + log.info("Retrying request on next coordinator") return self:send_on_next_coordinator(request) end diff --git a/src/cassandra/policies/reconnection.lua b/src/cassandra/policies/reconnection.lua index 9f9ad44..7cb8e75 100644 --- a/src/cassandra/policies/reconnection.lua +++ b/src/cassandra/policies/reconnection.lua @@ -18,7 +18,7 @@ local function shared_exponential_reconnection_policy(base_delay, max_delay) local index_key = "exp_reconnection_idx_"..host.address local dict = cache.get_dict(host.options.shm) - local ok, err = dict:set(index_key, 0) + local ok, err = dict:set(index_key, 1) if not ok then log.err("Cannot reset schedule for shared exponential reconnection policy: "..err) end @@ -27,7 +27,7 @@ local function shared_exponential_reconnection_policy(base_delay, max_delay) local index_key = "exp_reconnection_idx_"..host.address local dict = cache.get_dict(host.options.shm) - local ok, err = dict:add(index_key, 0) + local ok, err = dict:add(index_key, 1) if not ok and err ~= "exists" then log.err("Cannot prepare shared exponential reconnection policy: "..err) end diff --git a/src/cassandra/utils/table.lua b/src/cassandra/utils/table.lua index f897e68..c4572d1 100644 --- a/src/cassandra/utils/table.lua +++ b/src/cassandra/utils/table.lua @@ -1,6 +1,5 @@ local setmetatable = setmetatable local getmetatable = getmetatable -local rawget = rawget local tostring = tostring local pairs = pairs local type = type From c22303c5fc52a6a991712da4855c74f6ee4fccf2 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Tue, 24 Nov 2015 16:14:42 -0800 Subject: [PATCH 50/78] docs: rewrite README and update LICENSE --- LICENSE | 4 +- README.md | 193 ++++++++++++++++++++++++++++++++++-------------------- 2 files changed, 124 insertions(+), 73 deletions(-) diff --git a/LICENSE b/LICENSE index 4040943..e0286b5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ The MIT License (MIT) -Original work Copyright (c) 2014 Juarez Bochi -Modified work Copyright 2015 Thibault Charbonnier +Original work Copyright (c) 2015 Thibault Charbonnier +Based on the work of Juarez Bochi Copyright 2014 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 3d276e9..cc2a306 100644 --- a/README.md +++ b/README.md @@ -1,105 +1,156 @@ # lua-cassandra ![Module Version][badge-version-image] [![Build Status][badge-travis-image]][badge-travis-url] [![Coverage Status][badge-coveralls-image]][badge-coveralls-url] -> This project is a fork of [jbochi/lua-resty-cassandra][lua-resty-cassandra]. It adds support for binary protocol v3, a few bug fixes and more to come. See the improvements section for more details. +A pure Lua client library for Apache Cassandra (2.0+), compatible with Lua and [ngx_lua]. -Lua Cassandra client using CQL binary protocol v2/v3 for Cassandra 2.0 and later. +It is build on the model of the official Datastax drivers, and tries to implement the same behaviors and features. -It is 100% non-blocking if used in Nginx/Openresty but can also be used with luasocket. +## Features -## Installation - -#### Luarocks +- Leverage the ngx_lua cosocket API (non-blocking, reusable sockets) +- Fallback on LuaSocket for plain Lua compatibility +- Simple, prepared and batch statements +- Cluster topology automatic discovery +- Configurable load balancing, reconnection and retry policies +- TLS client-to-node encryption +- Client authentication +- Compatible with Cassandra 2.0 and 2.1 -Installation through [luarocks][luarocks-url] is recommended: +## Usage -```bash -$ luarocks install lua-cassandra +With ngx_lua: + +```nginx +http { + # you do not need the following line if you are using + # luarocks + lua_package_path "/path/to/src/?.lua;/path/to/src/?/init.lua;;"; + + # all cluster informations will be stored here + lua_shared_dict cassandra 1m; + + init_by_lua ' + local cassandra = require "cassandra" + + -- retrieve cluster topology + local ok, err = cassandra.spawn_cluster { + shm = "cassandra", -- defined by "lua_shared_dict" + contact_points = {"127.0.0.1", "127.0.0.2"} + } + if not ok then + ngx.log(ngx.ERR, "Could not spawn cluster: ", err.message) + end + '; + + server { + ... + + location /insert { + local cassandra = require "cassandra" + + local session, err = cassandra.spawn_session { + shm = "cassandra" -- defined by "lua_shared_dict" + } + if err then + ngx.log(ngx.ERR, "Could not spawn session: ", err.message) + return ngx.exit(500) + end + + local res, err = session:execute("INSERT INTO users(id, name, age) VALUES(?, ?, ?)", { + cassandra.uuid("1144bada-852c-11e3-89fb-e0b9a54a6d11"), + "John O'Reilly", + 42 + }) + if err then + -- ... + end + + session:set_keep_alive() + } + + location /get { + content_by_lua ' + local cassandra = require "cassandra" + + local session, err = cassandra.spawn_session { + shm = "cassandra" -- defined by "lua_shared_dict" + } + if err then + ngx.log(ngx.ERR, "Could not spawn session: ", err.message) + return ngx.exit(500) + end + + local rows, err = session:execute("SELECT * FROM users") + if err then + -- ... + end + + session:set_keep_alive() + + ngx.say("number of users: ", #rows) + '; + } + } +} ``` -#### Manual - -Simply copy the `src/` folder in your application. - -## Usage +With plain Lua: ```lua local cassandra = require "cassandra" --- local cassandra = require "cassandra.v2" -- binary protocol v2 for Cassandra 2.0.x -local session = cassandra:new() -session:set_timeout(1000) -- 1000ms timeout +local ok, err, cluster = cassandra.spawn_cluster { + shm = "cassandra", + contact_points = {"127.0.0.1", "127.0.0.2"} +} -local connected, err = session:connect("127.0.0.1", 9042) -assert(connected) -session:set_keyspace("demo") +local session, err = cluster:spawn_session() +assert(err == nil) --- simple query -local table_created, err = session:execute [[ - CREATE TABLE users( - id uuid PRIMARY KEY, - name varchar, - age int - ) -]] +local res, err = session:execute("INSERT INTO users(id, name, age) VALUES(?, ?, ?)", { + cassandra.uuid("1144bada-852c-11e3-89fb-e0b9a54a6d11"), + "John O'Reilly", + 42 +}) +assert(err == nil) --- query with arguments -local ok, err = session:execute("INSERT INTO users(name, age, user_id) VALUES(?, ?, ?)" - , {"John O'Reilly", 42, cassandra.uuid("1144bada-852c-11e3-89fb-e0b9a54a6d11")}) +local rows, err = session:execute("SELECT * FROM users") +assert(err == nil) +print("number of users: ", #rows) --- select statement -local users, err = session:execute("SELECT name, age, user_id FROM users") -assert(1 == #users) - -local user = users[1] -print(user.name) -- "John O'Reilly" -print(user.user_id) -- "1144bada-852c-11e3-89fb-e0b9a54a6d11" -print(user.age) -- 42 +session:shutdown() ``` -You can check more examples on the [documentation][documentation-reference] or in the [tests](https://github.com/thibaultcha/lua-cassandra/blob/master/spec/integration_spec.lua). - -## Documentation and examples - -Refer to the online [manual][documentation-manual] and [reference][documentation-reference]. - -## Improvements +## Installation -This fork provides the following improvements over the root project: +With [Luarocks]: -- [x] Support for binary protocol v3 - - [x] User Defined Types and Tuples support - - [x] Serial Consistency support for batch requests -- [x] Support for authentication -- [x] Keyspace switch fix -- [x] IPv6 encoding fix +```bash +$ luarocks install lua-cassandra +``` -## Roadmap +If installed manually, this module requires: -- [ ] Support for binary protocol v3 named values when binding a query -- [ ] Support for binary protocol v3 default timestamp option -- [ ] Support for binary protocol v4 +- [cjson](https://github.com/mpx/lua-cjson/) +- [LuaSocket](http://w3.impa.br/~diego/software/luasocket/) +- If you wish to use TLS client-to-node encryption, [LuaSec](https://github.com/brunoos/luasec) -## Makefile Operations +Once you have a local copy of this module's files under `src/`, add this to your Lua package path: -When developing, use the `Makefile` for doing the following operations: +``` +/path/to/src/?.lua;/path/to/src/?/init.lua; +``` -| Name | Description | -| -------------:| ----------------------------------------------| -| `dev` | Install busted, luacov and luacheck | -| `test` | Run the unit tests | -| `lint` | Lint all Lua files in the repo | -| `coverage` | Run unit tests + coverage report | -| `clean` | Clean coverage report | +## Documentation and examples -**Note:** Before running `make lint` or `make test` you will need to run `make dev`. +The current [documentation] targets version `0.3.6` only. `0.4.0` documentation should come soon. -**Note bis:** Tests are running for both binary protocol v2 and v3, so you must ensure to be running Cassandra `2.O` or later. +[ngx_lua]: https://github.com/openresty/lua-nginx-module -[luarocks-url]: https://luarocks.org +[Luarocks]: https://luarocks.org [lua-resty-cassandra]: https://github.com/jbochi/lua-resty-cassandra -[documentation-reference]: http://thibaultcha.github.io/lua-cassandra/ -[documentation-manual]: http://thibaultcha.github.io/lua-cassandra/manual/README.md.html +[documentation]: http://thibaultcha.github.io/lua-cassandra/ +[manual]: http://thibaultcha.github.io/lua-cassandra/manual/README.md.html [badge-travis-url]: https://travis-ci.org/thibaultCha/lua-cassandra [badge-travis-image]: https://img.shields.io/travis/thibaultCha/lua-cassandra.svg?style=flat From 6a0da78dd80aab0670cae2254abc0276e635d797 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Tue, 24 Nov 2015 16:22:10 -0800 Subject: [PATCH 51/78] fix(frame_reader) don't return columns metadata --- README.md | 1 + spec/integration/cassandra_spec.lua | 9 +++++++++ src/cassandra/frame_reader.lua | 5 ++++- src/cassandra/policies/reconnection.lua | 4 ++-- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cc2a306..50ebf6f 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ It is build on the model of the official Datastax drivers, and tries to implemen - Configurable load balancing, reconnection and retry policies - TLS client-to-node encryption - Client authentication +- Highly configurable options per session/request - Compatible with Cassandra 2.0 and 2.1 ## Usage diff --git a/spec/integration/cassandra_spec.lua b/spec/integration/cassandra_spec.lua index ba25a7c..a337dd7 100644 --- a/spec/integration/cassandra_spec.lua +++ b/spec/integration/cassandra_spec.lua @@ -241,6 +241,15 @@ describe("session", function() assert.equal(1, #rows) assert.equal("Bob", rows[1].name) end) + it("should return results with a `meta` property", function() + local rows, err = session:execute("SELECT * FROM users") + assert.falsy(err) + assert.truthy(rows) + assert.truthy(rows.meta) + assert.falsy(rows.meta.columns) + assert.falsy(rows.meta.columns_count) + assert.False(rows.meta.has_more_pages) + end) it("support somewhat heavier insertions", function() for i = 2, 10000 do local res, err = session:execute("INSERT INTO users(id, name, n) VALUES(2644bada-852c-11e3-89fb-e0b9a54a6d93, ?, ?)", {"Alice", i}) diff --git a/src/cassandra/frame_reader.lua b/src/cassandra/frame_reader.lua index 2bfe65c..ba3ef18 100644 --- a/src/cassandra/frame_reader.lua +++ b/src/cassandra/frame_reader.lua @@ -80,7 +80,10 @@ local RESULT_PARSERS = { local rows = { type = "ROWS", - meta = metadata + meta = { + has_more_pages = metadata.has_more_pages, + paging_state = metadata.paging_state + } } for _ = 1, rows_count do local row = {} diff --git a/src/cassandra/policies/reconnection.lua b/src/cassandra/policies/reconnection.lua index 7cb8e75..9f9ad44 100644 --- a/src/cassandra/policies/reconnection.lua +++ b/src/cassandra/policies/reconnection.lua @@ -18,7 +18,7 @@ local function shared_exponential_reconnection_policy(base_delay, max_delay) local index_key = "exp_reconnection_idx_"..host.address local dict = cache.get_dict(host.options.shm) - local ok, err = dict:set(index_key, 1) + local ok, err = dict:set(index_key, 0) if not ok then log.err("Cannot reset schedule for shared exponential reconnection policy: "..err) end @@ -27,7 +27,7 @@ local function shared_exponential_reconnection_policy(base_delay, max_delay) local index_key = "exp_reconnection_idx_"..host.address local dict = cache.get_dict(host.options.shm) - local ok, err = dict:add(index_key, 1) + local ok, err = dict:add(index_key, 0) if not ok and err ~= "exists" then log.err("Cannot prepare shared exponential reconnection policy: "..err) end From cc4fe19f9723d06ccad5b582d978c42a79ea701e Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Tue, 24 Nov 2015 16:38:51 -0800 Subject: [PATCH 52/78] chore(rockspec) bump version to 0.4.0 --- README.md | 49 +++++++++++------------ lua-cassandra-0.3.6-0.rockspec | 17 -------- lua-cassandra-0.4.0-0.rockspec | 72 ++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 41 deletions(-) delete mode 100644 lua-cassandra-0.3.6-0.rockspec create mode 100644 lua-cassandra-0.4.0-0.rockspec diff --git a/README.md b/README.md index 50ebf6f..650fd1b 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,7 @@ With ngx_lua: ```nginx http { - # you do not need the following line if you are using - # luarocks + # you do not need the following line if you are using luarocks lua_package_path "/path/to/src/?.lua;/path/to/src/?/init.lua;;"; # all cluster informations will be stored here @@ -46,26 +45,28 @@ http { ... location /insert { - local cassandra = require "cassandra" - - local session, err = cassandra.spawn_session { - shm = "cassandra" -- defined by "lua_shared_dict" - } - if err then - ngx.log(ngx.ERR, "Could not spawn session: ", err.message) - return ngx.exit(500) - end - - local res, err = session:execute("INSERT INTO users(id, name, age) VALUES(?, ?, ?)", { - cassandra.uuid("1144bada-852c-11e3-89fb-e0b9a54a6d11"), - "John O'Reilly", - 42 - }) - if err then - -- ... - end - - session:set_keep_alive() + content_by_lua ' + local cassandra = require "cassandra" + + local session, err = cassandra.spawn_session { + shm = "cassandra" -- defined by "lua_shared_dict" + } + if err then + ngx.log(ngx.ERR, "Could not spawn session: ", err.message) + return ngx.exit(500) + end + + local res, err = session:execute("INSERT INTO users(id, name, age) VALUES(?, ?, ?)", { + cassandra.uuid("1144bada-852c-11e3-89fb-e0b9a54a6d11"), + "John O Reilly", + 42 + }) + if err then + -- ... + end + + session:set_keep_alive() + '; } location /get { @@ -132,7 +133,7 @@ $ luarocks install lua-cassandra If installed manually, this module requires: -- [cjson](https://github.com/mpx/lua-cjson/) +- [lua-cjson](https://github.com/mpx/lua-cjson/) - [LuaSocket](http://w3.impa.br/~diego/software/luasocket/) - If you wish to use TLS client-to-node encryption, [LuaSec](https://github.com/brunoos/luasec) @@ -159,4 +160,4 @@ The current [documentation] targets version `0.3.6` only. `0.4.0` documentation [badge-coveralls-url]: https://coveralls.io/r/thibaultCha/lua-cassandra?branch=master [badge-coveralls-image]: https://coveralls.io/repos/thibaultCha/lua-cassandra/badge.svg?branch=master&style=flat -[badge-version-image]: https://img.shields.io/badge/version-0.3.6--0-blue.svg?style=flat +[badge-version-image]: https://img.shields.io/badge/version-0.4.0--0-blue.svg?style=flat diff --git a/lua-cassandra-0.3.6-0.rockspec b/lua-cassandra-0.3.6-0.rockspec deleted file mode 100644 index 0803503..0000000 --- a/lua-cassandra-0.3.6-0.rockspec +++ /dev/null @@ -1,17 +0,0 @@ -package = "lua-cassandra" -version = "0.4.0-0" -source = { - url = "git://github.com/thibaultCha/lua-cassandra", - tag = "0.4.0" -} -description = { - summary = "Lua Cassandra driver", - homepage = "http://thibaultcha.github.io/lua-cassandra", - license = "MIT" -} -build = { - type = "builtin", - modules = { - - } -} diff --git a/lua-cassandra-0.4.0-0.rockspec b/lua-cassandra-0.4.0-0.rockspec new file mode 100644 index 0000000..cb33731 --- /dev/null +++ b/lua-cassandra-0.4.0-0.rockspec @@ -0,0 +1,72 @@ +package = "lua-cassandra" +version = "0.4.0-0" +source = { + url = "git://github.com/thibaultCha/lua-cassandra", + tag = "0.4.0" +} +description = { + summary = "Lua Cassandra client library", + homepage = "http://thibaultcha.github.io/lua-cassandra", + license = "MIT" +} +dependencies = { + "luasocket ~> 2.0.2-6", + "lua-cjson ~> 2.1.0-1" +} +build = { + type = "builtin", + modules = { + ["cassandra"] = "src/cassandra.lua", + + ["cassandra.log"] = "src/cassandra/log.lua", + ["cassandra.cache"] = "src/cassandra/cache.lua", + ["cassandra.errors"] = "src/cassandra/errors.lua", + ["cassandra.options"] = "src/cassandra/options.lua", + ["cassandra.requests"] = "src/cassandra/requests.lua", + ["cassandra.frame_reader"] = "src/cassandra/frame_reader.lua", + + ["cassandra.buffer"] = "src/cassandra/buffer/init.lua", + ["cassandra.buffer.raw_buffer"] = "src/cassandra/buffer/raw_buffer.lua", + + ["cassandra.policies.retry"] = "src/cassandra/policies/retry.lua", + ["cassandra.policies.reconnection"] = "src/cassandra/policies/reconnection.lua", + ["cassandra.policies.load_balancing"] = "src/cassandra/policies/load_balancing.lua", + ["cassandra.policies.address_resolution"] = "src/cassandra/policies/address_resolution.lua", + + ["cassandra.auth"] = "src/cassandra/auth/init.lua", + ["cassandra.auth.plain_text_password"] = "src/cassandra/auth/plain_text_password.lua", + + ["cassandra.utils.bit"] = "src/cassandra/utils/bit.lua", + ["cassandra.utils.time"] = "src/cassandra/utils/time.lua", + ["cassandra.utils.table"] = "src/cassandra/utils/table.lua", + ["cassandra.utils.number"] = "src/cassandra/utils/number.lua", + ["cassandra.utils.string"] = "src/cassandra/utils/string.lua", + ["cassandra.utils.classic"] = "src/cassandra/utils/classic.lua", + + ["cassandra.types"] = "src/cassandra/types/init.lua", + ["cassandra.types.bigint"] = "src/cassandra/types/bigint.lua", + ["cassandra.types.boolean"] = "src/cassandra/types/boolean.lua", + ["cassandra.types.byte"] = "src/cassandra/types/byte.lua", + ["cassandra.types.bytes"] = "src/cassandra/types/bytes.lua", + ["cassandra.types.double"] = "src/cassandra/types/double.lua", + ["cassandra.types.float"] = "src/cassandra/types/float.lua", + ["cassandra.types.frame_header"] = "src/cassandra/types/frame_header.lua", + ["cassandra.types.inet"] = "src/cassandra/types/inet.lua", + ["cassandra.types.int"] = "src/cassandra/types/int.lua", + ["cassandra.types.long"] = "src/cassandra/types/long.lua", + ["cassandra.types.long_string"] = "src/cassandra/types/long_string.lua", + ["cassandra.types.map"] = "src/cassandra/types/map.lua", + ["cassandra.types.options"] = "src/cassandra/types/options.lua", + ["cassandra.types.raw"] = "src/cassandra/types/raw.lua", + ["cassandra.types.set"] = "src/cassandra/types/set.lua", + ["cassandra.types.short"] = "src/cassandra/types/short.lua", + ["cassandra.types.short_bytes"] = "src/cassandra/types/short_bytes.lua", + ["cassandra.types.string"] = "src/cassandra/types/string.lua", + ["cassandra.types.string_map"] = "src/cassandra/types/string_map.lua", + ["cassandra.types.tuple"] = "src/cassandra/types/tuple.lua", + ["cassandra.types.tuple_type"] = "src/cassandra/types/tuple_type.lua", + ["cassandra.types.udt"] = "src/cassandra/types/udt.lua", + ["cassandra.types.udt_type"] = "src/cassandra/types/udt_type.lua", + ["cassandra.types.uuid"] = "src/cassandra/types/uuid.lua" + } +} From 22f09b6a6b05698e963b6958523f242de13012ca Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Tue, 24 Nov 2015 19:08:36 -0800 Subject: [PATCH 53/78] feat: provide a cassandra.unset type --- spec/integration/cql_types_spec.lua | 36 ++++++++++++++++++++++++++++- spec/spec_utils.lua | 7 ------ spec/unit/options_spec.lua | 7 ++++++ src/cassandra.lua | 2 ++ src/cassandra/buffer/init.lua | 21 ++++++++++++----- src/cassandra/frame_reader.lua | 2 +- src/cassandra/options.lua | 14 ++++++----- src/cassandra/types/bytes.lua | 8 +++++++ 8 files changed, 76 insertions(+), 21 deletions(-) diff --git a/spec/integration/cql_types_spec.lua b/spec/integration/cql_types_spec.lua index 5f2b5a9..78a31c0 100644 --- a/spec/integration/cql_types_spec.lua +++ b/spec/integration/cql_types_spec.lua @@ -42,7 +42,6 @@ describe("CQL types integration", function() bigint_sample bigint, blob_sample blob, boolean_sample boolean, - decimal_sample decimal, double_sample double, float_sample float, int_sample int, @@ -92,6 +91,25 @@ describe("CQL types integration", function() end) end + it("[unset] should support unset (NULL)", function() + assert.truthy(cassandra.unset) + assert.equal("unset", cassandra.unset.type_id) + + local rows, err = session:execute("SELECT * FROM all_types WHERE id = ?", {cassandra.uuid(_UUID)}) + assert.falsy(err) + assert.truthy(rows) + assert.truthy(rows[1].ascii_sample) + + local res, err = session:execute("UPDATE all_types SET ascii_sample = ? WHERE id = ?", {cassandra.unset, cassandra.uuid(_UUID)}) + assert.falsy(err) + assert.truthy(res) + + rows, err = session:execute("SELECT * FROM all_types WHERE id = ?", {cassandra.uuid(_UUID)}) + assert.falsy(err) + assert.truthy(rows) + assert.falsy(rows[1].ascii_sample) + end) + it("[list] should be inserted and retrieved", function() for _, fixture in ipairs(utils.cql_map_fixtures) do local insert_query = string.format("INSERT INTO all_types(id, map_sample_%s_%s) VALUES(?, ?)", fixture.key_type_name, fixture.value_type_name) @@ -110,6 +128,22 @@ describe("CQL types integration", function() end end) + it("[map] should support empty table inserted as null", function() + local types = require "cassandra.types" + local insert_query = "INSERT INTO all_types(id, map_sample_text_int) VALUES(?, ?)" + local select_query = "SELECT * FROM all_types WHERE id = ?" + local fixture = {} + + local res, err = session:execute(insert_query, {cassandra.uuid(_UUID), cassandra.map(fixture)}) + assert.falsy(err) + assert.truthy(res) + + local rows, err = session:execute(select_query, {cassandra.uuid(_UUID)}) + assert.falsy(err) + assert.truthy(rows) + assert.falsy(rows[1].map_sample_text_int) + end) + it("[map] should be inserted and retrieved", function() for _, fixture in ipairs(utils.cql_list_fixtures) do local insert_query = string.format("INSERT INTO all_types(id, list_sample_%s) VALUES(?, ?)", fixture.type_name) diff --git a/spec/spec_utils.lua b/spec/spec_utils.lua index 3c41301..256d746 100644 --- a/spec/spec_utils.lua +++ b/spec/spec_utils.lua @@ -117,13 +117,6 @@ _M.cql_map_fixtures = { value_type = types.cql_types.int, value_type_name = "int", value = {k1 = 1, k2 = 2} - }, - { - key_type = types.cql_types.text, - key_type_name = "text", - value_type = types.cql_types.int, - value_type_name = "int", - value = {} } } diff --git a/spec/unit/options_spec.lua b/spec/unit/options_spec.lua index 40eb713..efb4073 100644 --- a/spec/unit/options_spec.lua +++ b/spec/unit/options_spec.lua @@ -20,6 +20,13 @@ describe("options parsing", function() cassandra.spawn_cluster({ shm = "test" }) + end, "contact_points option is required") + + assert.has_error(function() + cassandra.spawn_cluster({ + shm = "test", + contact_points = {} + }) end, "contact_points must contain at least one contact point") assert.has_error(function() diff --git a/src/cassandra.lua b/src/cassandra.lua index 31a6b7f..d6ede67 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -904,6 +904,8 @@ function types_mt:__index(key) return function(value) return {value = value, type_id = CQL_TYPES[key]} end + elseif key == "unset" then + return {value = "unset", type_id = "unset"} end return rawget(self, key) diff --git a/src/cassandra/buffer/init.lua b/src/cassandra/buffer/init.lua index 2c8404e..792949b 100644 --- a/src/cassandra/buffer/init.lua +++ b/src/cassandra/buffer/init.lua @@ -5,6 +5,7 @@ local cql_types = types.cql_types local math_floor = math.floor local type = type +local assert = assert --- Frame types -- @section frame_types @@ -90,8 +91,10 @@ for _, cql_decoder in pairs(CQL_DECODERS) do end Buffer["read_cql_"..cql_decoder] = function(self, ...) local bytes = self:read_bytes() - local buf = Buffer(self.version, bytes) - return mod.read(buf, ...) + if bytes then + local buf = Buffer(self.version, bytes) + return mod.read(buf, ...) + end end if ALIASES[cql_decoder] ~= nil then @@ -113,6 +116,7 @@ function Buffer:repr_cql_value(value) else infered_type = cql_types.float end + -- infered type elseif lua_type == "table" then if t_utils.is_array(value) then infered_type = cql_types.set @@ -126,6 +130,10 @@ function Buffer:repr_cql_value(value) infered_type = cql_types.varchar end + if infered_type == "unset" then + return self:repr_bytes({unset = true}) + end + local encoder = "repr_cql_"..CQL_DECODERS[infered_type] return Buffer[encoder](self, value) end @@ -135,10 +143,11 @@ function Buffer:write_cql_value(...) end function Buffer:read_cql_value(assumed_type) - if CQL_DECODERS[assumed_type.type_id] == nil then - error("ATTEMPT TO USE NON IMPLEMENTED DECODER FOR TYPE ID: "..assumed_type.type_id) - end - local decoder = "read_cql_"..CQL_DECODERS[assumed_type.type_id] + local decoder_type = CQL_DECODERS[assumed_type.type_id] + + assert(decoder_type ~= nil, "No decoder for type id "..assumed_type.type_id) + + local decoder = "read_cql_"..decoder_type return Buffer[decoder](self, assumed_type.value_type_id) end diff --git a/src/cassandra/frame_reader.lua b/src/cassandra/frame_reader.lua index ba3ef18..af9dde7 100644 --- a/src/cassandra/frame_reader.lua +++ b/src/cassandra/frame_reader.lua @@ -88,7 +88,7 @@ local RESULT_PARSERS = { for _ = 1, rows_count do local row = {} for i = 1, columns_count do - row[columns[i].name] = buffer:read_cql_value(columns[i].type) + row[columns[i].name] = buffer:read_cql_value(columns[i].type) end rows[#rows + 1] = row end diff --git a/src/cassandra/options.lua b/src/cassandra/options.lua index 72ebae1..0ccae96 100644 --- a/src/cassandra/options.lua +++ b/src/cassandra/options.lua @@ -6,10 +6,10 @@ local utils = require "cassandra.utils.table" -- Nil values are stubs for the sole purpose of documenting their availability. local DEFAULTS = { - shm = nil, - prepared_shm = nil, - contact_points = {}, - keyspace = nil, + -- shm = nil, + -- prepared_shm = nil, + -- contact_points = {}, + -- keyspace = nil, policies = { address_resolution = require "cassandra.policies.address_resolution", load_balancing = require("cassandra.policies.load_balancing").SharedRoundRobin, @@ -33,8 +33,8 @@ local DEFAULTS = { connect_timeout = 1000, read_timeout = 2000 }, - username = nil, - password = nil, + -- username = nil, + -- password = nil, -- ssl_options = { -- key = nil, -- certificate = nil, @@ -71,6 +71,8 @@ end local function parse_cluster(options) parse_session(options) + assert(options.contact_points ~= nil, "contact_points option is required") + if type(options.contact_points) ~= "table" then error("contact_points must be a table", 3) end diff --git a/src/cassandra/types/bytes.lua b/src/cassandra/types/bytes.lua index 9f4e79c..bb6916e 100644 --- a/src/cassandra/types/bytes.lua +++ b/src/cassandra/types/bytes.lua @@ -1,11 +1,19 @@ local int = require "cassandra.types.int" +local type = type return { repr = function(self, val) + if type(val) == "table" and val.unset then + return int.repr(nil, -2) + end + return int.repr(nil, #val)..val end, read = function(self) local n_bytes = self:read_int() + if n_bytes < 0 then + return nil + end return self:read(n_bytes) end } From a18cb697fbfb50fb0d78b20f83140f97fef7067a Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Wed, 25 Nov 2015 16:31:58 -0800 Subject: [PATCH 54/78] perf(prepared) mutex on prepare query to avoid cold cache --- spec/integration/cassandra_spec.lua | 26 +++++++- spec/integration/cql_types_spec.lua | 1 - spec/prepare.lua | 12 ++-- src/cassandra.lua | 97 ++++++++++++++++++++--------- src/cassandra/cache.lua | 4 +- src/cassandra/log.lua | 1 + src/cassandra/utils/time.lua | 28 ++++++--- 7 files changed, 121 insertions(+), 48 deletions(-) diff --git a/spec/integration/cassandra_spec.lua b/spec/integration/cassandra_spec.lua index a337dd7..373fe6b 100644 --- a/spec/integration/cassandra_spec.lua +++ b/spec/integration/cassandra_spec.lua @@ -6,8 +6,10 @@ local utils = require "spec.spec_utils" local cassandra = require "cassandra" +local LOG_LVL = "ERR" + -- Define log level for tests -utils.set_log_lvl("ERR") +utils.set_log_lvl(LOG_LVL) local _shm = "cassandra_specs" @@ -41,6 +43,11 @@ describe("spawn cluster", function() end end) it("should iterate over contact_points to find an entrance into the cluster", function() + utils.set_log_lvl("QUIET") + finally(function() + utils.set_log_lvl(LOG_LVL) + end) + local contact_points = {"0.0.0.1", "0.0.0.2", "0.0.0.3"} contact_points[#contact_points + 1] = utils.contact_points[1] @@ -52,6 +59,11 @@ describe("spawn cluster", function() assert.True(ok) end) it("should return an error when no contact_point is valid", function() + utils.set_log_lvl("QUIET") + finally(function() + utils.set_log_lvl(LOG_LVL) + end) + local contact_points = {"0.0.0.1", "0.0.0.2", "0.0.0.3"} local ok, err = cassandra.spawn_cluster({ shm = "test", @@ -63,6 +75,11 @@ describe("spawn cluster", function() assert.equal("All hosts tried for query failed. 0.0.0.1: No route to host. 0.0.0.2: No route to host. 0.0.0.3: No route to host.", err.message) end) it("should accept a custom port for given hosts", function() + utils.set_log_lvl("QUIET") + finally(function() + utils.set_log_lvl(LOG_LVL) + end) + local contact_points = {} for i, addr in ipairs(utils.contact_points) do contact_points[i] = addr..":9043" @@ -76,6 +93,11 @@ describe("spawn cluster", function() assert.equal("NoHostAvailableError", err.type) end) it("should accept a custom port through an option", function() + utils.set_log_lvl("QUIET") + finally(function() + utils.set_log_lvl(LOG_LVL) + end) + local ok, err = cassandra.spawn_cluster({ shm = "test", protocol_options = {default_port = 9043}, @@ -545,7 +567,7 @@ describe("session", function() }, {prepare = true}) assert.falsy(err) - assert.spy(cache.get_prepared_query_id).was.called(8) + assert.spy(cache.get_prepared_query_id).was.called(8 + 2) -- twice called for double check after mutex in ngx_lua assert.spy(cache.set_prepared_query_id).was.called(2) local rows, err = session:execute("SELECT name FROM users WHERE id = ? AND n = ?", {cassandra.uuid(_UUID), 6}) diff --git a/spec/integration/cql_types_spec.lua b/spec/integration/cql_types_spec.lua index 78a31c0..f724355 100644 --- a/spec/integration/cql_types_spec.lua +++ b/spec/integration/cql_types_spec.lua @@ -129,7 +129,6 @@ describe("CQL types integration", function() end) it("[map] should support empty table inserted as null", function() - local types = require "cassandra.types" local insert_query = "INSERT INTO all_types(id, map_sample_text_int) VALUES(?, ?)" local select_query = "SELECT * FROM all_types WHERE id = ?" local fixture = {} diff --git a/spec/prepare.lua b/spec/prepare.lua index 46e22ee..ad7d74b 100644 --- a/spec/prepare.lua +++ b/spec/prepare.lua @@ -5,10 +5,10 @@ local log = require "cassandra.log" log.set_lvl("ERR") -local _, err, cluster = cassandra.spawn_cluster {shm = "cassandra", contact_points = {"127.0.0.1", "127.0.0.2"}} +local _, err, cluster = cassandra.spawn_cluster {shm = "cassandra", contact_points = {"127.0.0.1"}} assert(err == nil, inspect(err)) -local session, err = cluster:spawn_session() +local session, err = cluster:spawn_session({keyspace = "page"}) assert(err == nil, inspect(err)) -- for i = 1, 10000 do @@ -20,18 +20,18 @@ assert(err == nil, inspect(err)) local start, total -start = os.clock() +start = os.time() for rows, err, page in session:execute("SELECT * FROM users", nil, {page_size = 20, auto_paging = true}) do end -total = os.clock() - start +total = os.time() - start print("Time without prepared = "..total) -start = os.clock() +start = os.time() for rows, err, page in session:execute("SELECT * FROM users", nil, {page_size = 20, auto_paging = true, prepare = true}) do end -total = os.clock() - start +total = os.time() - start print("Time with prepared = "..total) diff --git a/src/cassandra.lua b/src/cassandra.lua index d6ede67..1e3953d 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -25,6 +25,30 @@ local table_insert = table.insert local string_format = string.format local setmetatable = setmetatable +local is_ngx = ngx ~= nil + +local function lock_mutex(shm, key) + if is_ngx then + local resty_lock = require "resty.lock" + local lock = resty_lock:new(shm) + local _, err = lock:lock(key) + if err then + err = "Error locking mutex: "..err + end + return lock, err + end +end + +local function unlock_mutex(lock) + if is_ngx then + local ok, err = lock:unlock() + if not ok then + err = "Error unlocking mutex: "..err + end + return err + end +end + --- Constants -- @section constants @@ -138,14 +162,14 @@ function Host:send(request) end local function startup(self) - log.info("Startup request. Trying to use protocol v"..self.protocol_version) + log.debug("Startup request. Trying to use protocol v"..self.protocol_version) local startup_req = Requests.StartupRequest() return self:send(startup_req) end local function change_keyspace(self, keyspace) - log.info("Keyspace request. Using keyspace: "..keyspace) + log.debug("Keyspace request. Using keyspace: "..keyspace) local keyspace_req = Requests.KeyspaceRequest(keyspace) return self:send(keyspace_req) @@ -206,13 +230,13 @@ end function Host:connect() if self.connected then return true end - log.info("Connecting to "..self.address) + log.debug("Connecting to "..self.address) self:set_timeout(self.options.socket_options.connect_timeout) local ok, err = self.socket:connect(self.host, self.port) if ok ~= 1 then - log.info("Could not connect to "..self.address..". Reason: "..err) + log.err("Could not connect to "..self.address..". Reason: "..err) return false, err, true end @@ -223,7 +247,7 @@ function Host:connect() end end - log.info("Session connected to "..self.address) + log.debug("Session connected to "..self.address) if self:get_reused_times() > 0 then -- No need for startup request @@ -267,7 +291,7 @@ function Host:connect() end if ready then - log.info("Host at "..self.address.." is ready with protocol v"..self.protocol_version) + log.debug("Host at "..self.address.." is ready with protocol v"..self.protocol_version) if self.options.keyspace ~= nil then local _, err = change_keyspace(self, self.options.keyspace) @@ -335,7 +359,7 @@ function Host:set_keep_alive() end function Host:close() - -- don't close if the connection was not opened yet + -- don't try to close if the connection was not opened yet if not self.connected then return true end @@ -403,14 +427,7 @@ function Host:can_be_considered_up() return nil, err end - local delay = time_utils.get_time() - host_infos.unhealthy_at - - if is_up then - return true - elseif (delay >= host_infos.reconnection_delay) then - log.info("Host "..self.address.." could nbe considered up after "..delay.."ms") - return true - end + return is_up or (time_utils.get_time() - host_infos.unhealthy_at >= host_infos.reconnection_delay) end --- Request Handler @@ -600,11 +617,12 @@ end function RequestHandler:retry(request) self.n_retries = self.n_retries + 1 - log.info("Retrying request on next coordinator") + log.debug("Retrying request on next coordinator") return self:send_on_next_coordinator(request) end function RequestHandler:prepare_and_retry(request) + log.info("Query 0x"..request:hex_query_id().." not prepared on host "..self.coordinator.address..". Preparing and retrying.") local query = request.query local prepare_request = Requests.PrepareRequest(query) @@ -650,23 +668,45 @@ function Session:new(options) end local function prepare_query(request_handler, query) - local query_id, cache_err = cache.get_prepared_query_id(request_handler.options, query) + -- If the query is found in the cache, all workers can access it. + -- If it is not found, we might be in a cold-cache scenario. In that case, + -- only one worker needs to + local query_id, cache_err, prepared_key = cache.get_prepared_query_id(request_handler.options, query) if cache_err then return nil, cache_err elseif query_id == nil then - log.info("Query not prepared in cluster yet. Preparing.") - local prepare_request = Requests.PrepareRequest(query) - local res, err = request_handler:send(prepare_request) - if err then - return nil, err + -- MUTEX + local prepared_key_lock = prepared_key.."_lock" + local lock, lock_err = lock_mutex(request_handler.options.prepared_shm, prepared_key_lock) + if lock_err then + return nil, lock_err end - query_id = res.query_id - local ok, cache_err = cache.set_prepared_query_id(request_handler.options, query, query_id) - if not ok then + -- once the lock is resolved, all other workers can retry to get the query, and should + -- instantly succeed. We then skip the preparation part. + query_id, cache_err = cache.get_prepared_query_id(request_handler.options, query) + if cache_err then return nil, cache_err + elseif query_id == nil then + log.info("Query not prepared in cluster yet. Preparing.") + local prepare_request = Requests.PrepareRequest(query) + local res, err = request_handler:send(prepare_request) + if err then + return nil, err + end + query_id = res.query_id + local ok, cache_err = cache.set_prepared_query_id(request_handler.options, query, query_id) + if not ok then + return nil, cache_err + end + log.info("Query prepared for host "..request_handler.coordinator.address) + end + + -- UNLOCK MUTEX + local lock_err = unlock_mutex(lock) + if lock_err then + return nil, "Error unlocking mutex for query preparation: "..lock_err end - log.info("Query prepared for host "..request_handler.coordinator.address) end return query_id @@ -869,8 +909,9 @@ end local Cluster = {} Cluster.__index = Cluster -function Cluster:spawn_session() - return Cassandra.spawn_session(self.options) +function Cluster:spawn_session(options) + options = table_utils.extend_table(self.options, options) + return Cassandra.spawn_session(options) end --- Retrieve cluster informations and store them in ngx.shared.DICT diff --git a/src/cassandra/cache.lua b/src/cassandra/cache.lua index c5e3d06..32f1215 100644 --- a/src/cassandra/cache.lua +++ b/src/cassandra/cache.lua @@ -172,7 +172,7 @@ local function set_prepared_query_id(options, query, query_id) if not ok then err = "Cannot store prepared query id in shm "..shm..": "..err elseif forcible then - log.warn("Prepared shm "..shm.." running out of memory. Consider increasing its size.") + log.warn("shm for prepared queries '"..shm.."' is running out of memory. Consider increasing its size.") dict:flush_expired(1) end return ok, err @@ -187,7 +187,7 @@ local function get_prepared_query_id(options, query) if err then err = "Cannot retrieve prepared query id in shm "..shm..": "..err end - return value, err + return value, err, prepared_key end return { diff --git a/src/cassandra/log.lua b/src/cassandra/log.lua index c14121d..e7e7796 100644 --- a/src/cassandra/log.lua +++ b/src/cassandra/log.lua @@ -10,6 +10,7 @@ local string_format = string.format -- ngx_lua levels redefinition for helpers and -- when outside of ngx_lua. local LEVELS = { + QUIET = 0, ERR = 1, WARN = 2, INFO = 3, diff --git a/src/cassandra/utils/time.lua b/src/cassandra/utils/time.lua index 33cd225..af2cb28 100644 --- a/src/cassandra/utils/time.lua +++ b/src/cassandra/utils/time.lua @@ -1,26 +1,36 @@ -local type = type local exec = os.execute -local ngx_sleep +local time = os.time + +local sleep +local now +local ngx_get_phase local is_ngx = ngx ~= nil if is_ngx then - ngx_sleep = ngx.sleep + sleep = ngx.sleep + now = ngx.now + ngx_get_phase = ngx.get_phase end local function get_time() - if ngx and type(ngx.now) == "function" then - return ngx.now() * 1000 + if is_ngx and ngx_get_phase() ~= "init" then + return now() * 1000 else - return os.time() * 1000 + return time() * 1000 end end local function wait(t) if t == nil then t = 0.5 end + if is_ngx then - ngx_sleep(t) - else - exec("sleep "..t) + local phase = ngx_get_phase() + if phase == "rewrite" or phase == "access" or phase == "content" then + sleep(t) + return + end end + + exec("sleep "..t) end return { From 0e48ee0c8e92d490c1e1b336695ecfe52d54ce47 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Sun, 6 Dec 2015 16:22:35 -0800 Subject: [PATCH 55/78] improve driver under heavy load - mutexs to avoid race conditions accessing the underlying shm in cache zhen setting hsot as UP/DOWN - smater logs to avoid flooding the error.log file - fix round robin load balancing policy which could result in returning the same index multiple times during an iteration if the policy was used by many workers at the same time. --- .luacheckrc | 2 +- spec/integration/cassandra_spec.lua | 2 +- spec/unit/load_balancing_policy_spec.lua | 23 ++++- src/cassandra.lua | 109 ++++++++++++++-------- src/cassandra/policies/load_balancing.lua | 18 ++-- t/00-load-balancing-policies.t | 2 +- 6 files changed, 105 insertions(+), 51 deletions(-) diff --git a/.luacheckrc b/.luacheckrc index ce069b7..efffb5d 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -1,3 +1,3 @@ unused_args = false redefined = false -globals = {"ngx", "describe", "setup", "teardown", "it", "pending", "after_each", "finally", "spy"} +globals = {"ngx", "describe", "setup", "teardown", "it", "pending", "before_each", "after_each", "finally", "spy"} diff --git a/spec/integration/cassandra_spec.lua b/spec/integration/cassandra_spec.lua index 373fe6b..ceb03e1 100644 --- a/spec/integration/cassandra_spec.lua +++ b/spec/integration/cassandra_spec.lua @@ -567,7 +567,7 @@ describe("session", function() }, {prepare = true}) assert.falsy(err) - assert.spy(cache.get_prepared_query_id).was.called(8 + 2) -- twice called for double check after mutex in ngx_lua + assert.spy(cache.get_prepared_query_id).was.called(8) assert.spy(cache.set_prepared_query_id).was.called(2) local rows, err = session:execute("SELECT name FROM users WHERE id = ? AND n = ?", {cassandra.uuid(_UUID), 6}) diff --git a/spec/unit/load_balancing_policy_spec.lua b/spec/unit/load_balancing_policy_spec.lua index c06211b..383728f 100644 --- a/spec/unit/load_balancing_policy_spec.lua +++ b/spec/unit/load_balancing_policy_spec.lua @@ -1,3 +1,4 @@ +local cache = require "cassandra.cache" local load_balancing_policies = require "cassandra.policies.load_balancing" describe("Load balancing policies", function() @@ -6,18 +7,34 @@ describe("Load balancing policies", function() local shm = "cassandra" local hosts = {"127.0.0.1", "127.0.0.2", "127.0.0.3"} + before_each(function() + local dict = cache.get_dict(shm) + dict:flush_all() + dict:flush_expired() + end) + it("should iterate over the hosts in a round robin fashion", function() local iter = SharedRoundRobin(shm, hosts) assert.equal("127.0.0.1", select(2, iter())) assert.equal("127.0.0.2", select(2, iter())) assert.equal("127.0.0.3", select(2, iter())) end) - it("should share its state accros different iterators", function() + it("should start at a different indexes for each iterator", function() local iter1 = SharedRoundRobin(shm, hosts) local iter2 = SharedRoundRobin(shm, hosts) - assert.equal("127.0.0.1", select(2, iter1())) - assert.equal("127.0.0.2", select(2, iter2())) + local iter3 = SharedRoundRobin(shm, hosts) + + assert.equal("127.0.0.1", select(2, iter1())) -- iter 1 starts on index 1 + assert.equal("127.0.0.2", select(2, iter2())) -- iter 2 starts on index 2 + assert.equal("127.0.0.3", select(2, iter3())) -- iter 3 starts on index 3 + + assert.equal("127.0.0.2", select(2, iter1())) assert.equal("127.0.0.3", select(2, iter1())) + + assert.equal("127.0.0.3", select(2, iter2())) + assert.equal("127.0.0.1", select(2, iter3())) + assert.equal("127.0.0.1", select(2, iter2())) + assert.equal("127.0.0.2", select(2, iter3())) end) it("should be callable in a loop", function() assert.has_no_errors(function() diff --git a/src/cassandra.lua b/src/cassandra.lua index 1e3953d..ed2ce24 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -31,11 +31,13 @@ local function lock_mutex(shm, key) if is_ngx then local resty_lock = require "resty.lock" local lock = resty_lock:new(shm) - local _, err = lock:lock(key) + local elapsed, err = lock:lock(key) if err then err = "Error locking mutex: "..err end - return lock, err + return lock, err, elapsed + else + return nil, nil, 0 end end @@ -236,7 +238,7 @@ function Host:connect() local ok, err = self.socket:connect(self.host, self.port) if ok ~= 1 then - log.err("Could not connect to "..self.address..". Reason: "..err) + --log.err("Could not connect to "..self.address..". Reason: "..err) return false, err, true end @@ -376,18 +378,34 @@ function Host:close() end function Host:set_down() - log.warn("Setting host "..self.address.." as DOWN") local host_infos, err = cache.get_host(self.options.shm, self.address) if err then - return false, err + return err end - host_infos.unhealthy_at = time_utils.get_time() - host_infos.reconnection_delay = self.reconnection_policy.next(self) - self:close() - new_socket(self) + if host_infos.unhealthy_at == 0 then + local lock, lock_err, elapsed = lock_mutex(self.options.shm, "downing_"..self.address) + if lock_err then + return lock_err + end - return cache.set_host(self.options.shm, self.address, host_infos) + if elapsed and elapsed == 0 then + log.warn("Setting host "..self.address.." as DOWN") + host_infos.unhealthy_at = time_utils.get_time() + host_infos.reconnection_delay = self.reconnection_policy.next(self) + self:close() + new_socket(self) + local ok, err = cache.set_host(self.options.shm, self.address, host_infos) + if not ok then + return err + end + end + + lock_err = unlock_mutex(lock) + if lock_err then + return err + end + end end function Host:set_up() @@ -396,8 +414,8 @@ function Host:set_up() return false, err end - -- host was previously marked a DOWN - if host_infos.unhealthy_at ~= 0 then + -- host was previously marked as DOWN (+ a safe delay) + if host_infos.unhealthy_at ~= 0 and time_utils.get_time() - host_infos.unhealthy_at >= host_infos.reconnection_delay then log.warn("Setting host "..self.address.." as UP") host_infos.unhealthy_at = 0 -- reset schedule for reconnection delay @@ -464,7 +482,7 @@ function RequestHandler:get_next_coordinator() local errors = {} local iter = self.options.policies.load_balancing - for _, host in iter(self.options.shm, self.hosts) do + for i, host in iter(self.options.shm, self.hosts) do local can_host_be_considered_up, cache_err = host:can_be_considered_up() if cache_err then return nil, cache_err @@ -477,8 +495,8 @@ function RequestHandler:get_next_coordinator() if maybe_down then -- only on socket connect error -- might be a bad host, setting DOWN - local ok, cache_err = host:set_down() - if not ok then + local cache_err = host:set_down() + if cache_err then return nil, cache_err end end @@ -536,7 +554,7 @@ end function RequestHandler:send_on_next_coordinator(request) local coordinator, err = self:get_next_coordinator() - if err then + if not coordinator or err then return nil, err end @@ -577,8 +595,8 @@ function RequestHandler:handle_error(request, err) if err.type == "SocketError" then -- host seems unhealthy - local ok, cache_err = self.coordinator:set_down() - if not ok then + local cache_err = self.coordinator:set_down() + if cache_err then return nil, cache_err end -- always retry, another node will be picked @@ -617,24 +635,38 @@ end function RequestHandler:retry(request) self.n_retries = self.n_retries + 1 - log.debug("Retrying request on next coordinator") + log.info("Retrying request on next coordinator") return self:send_on_next_coordinator(request) end function RequestHandler:prepare_and_retry(request) + local preparing_lock_key = string_format("0x%s_host_%s", request.query_id, self.coordinator.address) - log.info("Query 0x"..request:hex_query_id().." not prepared on host "..self.coordinator.address..". Preparing and retrying.") - local query = request.query - local prepare_request = Requests.PrepareRequest(query) - local res, err = self:send(prepare_request) - if err then - return nil, err + -- MUTEX + local lock, lock_err, elapsed = lock_mutex(self.options.prepared_shm, preparing_lock_key) + if lock_err then + return nil, lock_err + end + + if elapsed and elapsed == 0 then + -- prepare on this host + log.info("Query 0x"..request:hex_query_id().." not prepared on host "..self.coordinator.address..". Preparing and retrying.") + local prepare_request = Requests.PrepareRequest(request.query) + local res, err = self:send(prepare_request) + if err then + return nil, err + end + log.info("Query prepared for host "..self.coordinator.address) + + if request.query_id ~= res.query_id then + log.warn(string_format("Unexpected difference between prepared query ids for query %s (%s ~= %s)", request.query, request.query_id, res.query_id)) + request.query_id = res.query_id + end end - log.info("Query prepared for host "..self.coordinator.address) - if request.query_id ~= res.query_id then - log.warn(string_format("Unexpected difference between query ids for query %s (%s ~= %s)", query, request.query_id, res.query_id)) - request.query_id = res.query_id + lock_err = unlock_mutex(lock) + if lock_err then + return nil, "Error unlocking mutex for query preparation: "..lock_err end -- Send on the same coordinator as the one it was just prepared on @@ -677,17 +709,13 @@ local function prepare_query(request_handler, query) elseif query_id == nil then -- MUTEX local prepared_key_lock = prepared_key.."_lock" - local lock, lock_err = lock_mutex(request_handler.options.prepared_shm, prepared_key_lock) + local lock, lock_err, elapsed = lock_mutex(request_handler.options.prepared_shm, prepared_key_lock) if lock_err then return nil, lock_err end - -- once the lock is resolved, all other workers can retry to get the query, and should - -- instantly succeed. We then skip the preparation part. - query_id, cache_err = cache.get_prepared_query_id(request_handler.options, query) - if cache_err then - return nil, cache_err - elseif query_id == nil then + if elapsed and elapsed == 0 then + -- prepare query in the mutex log.info("Query not prepared in cluster yet. Preparing.") local prepare_request = Requests.PrepareRequest(query) local res, err = request_handler:send(prepare_request) @@ -700,10 +728,17 @@ local function prepare_query(request_handler, query) return nil, cache_err end log.info("Query prepared for host "..request_handler.coordinator.address) + else + -- once the lock is resolved, all other workers can retry to get the query, and should + -- instantly succeed. We then skip the preparation part. + query_id, cache_err = cache.get_prepared_query_id(request_handler.options, query) + if cache_err then + return nil, cache_err + end end -- UNLOCK MUTEX - local lock_err = unlock_mutex(lock) + lock_err = unlock_mutex(lock) if lock_err then return nil, "Error unlocking mutex for query preparation: "..lock_err end diff --git a/src/cassandra/policies/load_balancing.lua b/src/cassandra/policies/load_balancing.lua index 5693037..a6acce5 100644 --- a/src/cassandra/policies/load_balancing.lua +++ b/src/cassandra/policies/load_balancing.lua @@ -8,25 +8,27 @@ return { local counter = 0 local dict = cache.get_dict(shm) - local ok, err = dict:add("rr_plan_index", 0) + + local ok, err = dict:add("rr_index", -1) if not ok and err ~= "exists" then log.err("Cannot prepare shared round robin load balancing policy: "..err) end + local index, err = dict:incr("rr_index", 1) + if err then + log.err("Cannot prepare shared round robin load balancing policy: "..err) + end + + local plan_index = math_fmod(index or 0, n) + return function(t, i) - local plan_index = dict:get("rr_plan_index") local mod = math_fmod(plan_index, n) + 1 - dict:incr("rr_plan_index", 1) + plan_index = plan_index + 1 counter = counter + 1 if counter <= n then return mod, hosts[mod] end - - local ok, err = dict:set("rr_plan_index", 0) - if not ok then - log.err("Cannot reset shared round robin load balancing policy: "..err) - end end end } diff --git a/t/00-load-balancing-policies.t b/t/00-load-balancing-policies.t index 91a791e..3b3d04c 100644 --- a/t/00-load-balancing-policies.t +++ b/t/00-load-balancing-policies.t @@ -1,7 +1,7 @@ use Test::Nginx::Socket::Lua; use Cwd qw(cwd); -repeat_each(3); +repeat_each(1); plan tests => repeat_each() * blocks() * 3; From 81cd350790787bd3ed1351f96ae30aa51e73aafb Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Sun, 6 Dec 2015 16:40:36 -0800 Subject: [PATCH 56/78] chore(ci) fix travis ci builds --- .ci/busted_print.lua | 21 ++++++ {.travis => .ci}/platform.sh | 0 .ci/setup_cassandra.sh | 12 ++++ .ci/setup_lua.sh | 106 ++++++++++++++++++++++++++++ .travis.yml | 34 ++++++--- .travis/setup_cassandra.sh | 7 -- .travis/setup_lua.sh | 101 -------------------------- Makefile | 7 +- lua-cassandra-0.4.0-0.rockspec | 2 +- spec/integration/cassandra_spec.lua | 40 +++++------ spec/integration/cql_types_spec.lua | 11 +-- spec/spec_utils.lua | 16 ++++- src/cassandra/options.lua | 2 +- src/cassandra/requests.lua | 6 ++ 14 files changed, 218 insertions(+), 147 deletions(-) create mode 100644 .ci/busted_print.lua rename {.travis => .ci}/platform.sh (100%) create mode 100755 .ci/setup_cassandra.sh create mode 100755 .ci/setup_lua.sh delete mode 100755 .travis/setup_cassandra.sh delete mode 100755 .travis/setup_lua.sh diff --git a/.ci/busted_print.lua b/.ci/busted_print.lua new file mode 100644 index 0000000..ecc9493 --- /dev/null +++ b/.ci/busted_print.lua @@ -0,0 +1,21 @@ +local ansicolors = require 'ansicolors' + +return function(options) + local handler = require 'busted.outputHandlers.utfTerminal'(options) + + handler.fileStart = function(file) + io.write('\n' .. ansicolors('%{cyan}' .. file.name) .. ':') + end + + handler.testStart = function(element, parent, status, debug) + io.write('\n ' .. handler.getFullName(element) .. ' ... ') + io.flush() + end + + local busted = require 'busted' + + busted.subscribe({ 'file', 'start' }, handler.fileStart) + busted.subscribe({ 'test', 'start' }, handler.testStart) + + return handler +end diff --git a/.travis/platform.sh b/.ci/platform.sh similarity index 100% rename from .travis/platform.sh rename to .ci/platform.sh diff --git a/.ci/setup_cassandra.sh b/.ci/setup_cassandra.sh new file mode 100755 index 0000000..afa2177 --- /dev/null +++ b/.ci/setup_cassandra.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +arr=(${HOSTS//,/ }) + +pip install --user PyYAML six +git clone https://github.com/pcmanus/ccm.git +pushd ccm +./setup.py install --user +popd +ccm create test -v binary:$CASSANDRA_VERSION -n ${#arr[@]} -d +ccm start -v +ccm status diff --git a/.ci/setup_lua.sh b/.ci/setup_lua.sh new file mode 100755 index 0000000..fc08986 --- /dev/null +++ b/.ci/setup_lua.sh @@ -0,0 +1,106 @@ +#! /bin/bash + +# A script for setting up environment for travis-ci testing. +# Sets up Lua and Luarocks. +# LUA must be "lua5.1", "lua5.2" or "luajit". +# luajit2.0 - master v2.0 +# luajit2.1 - master v2.1 + +LUAJIT="no" + +source .ci/platform.sh +cd $HOME + +############ +# Lua/LuaJIT +############ + +if [ "$PLATFORM" == "macosx" ]; then + if [ "$LUA" == "luajit" ]; then + LUAJIT="yes" + fi + if [ "$LUA" == "luajit2.0" ]; then + LUAJIT="yes" + fi + if [ "$LUA" == "luajit2.1" ]; then + LUAJIT="yes" + fi +elif [ "$(expr substr $LUA 1 6)" == "luajit" ]; then + LUAJIT="yes" +fi + +if [ "$LUAJIT" == "yes" ]; then + + mkdir -p $LUAJIT_DIR + + # If cache is empty, downlaod and compile + if [ ! "$(ls -A $LUAJIT_DIR)" ]; then + + LUAJIT_BASE="LuaJIT-2.0.4" + cd $LUAJIT_DIR + + if [ "$LUA" == "luajit" ]; then + curl http://luajit.org/download/$LUAJIT_BASE.tar.gz | tar xz + else + git clone http://luajit.org/git/luajit-2.0.git $LUAJIT_BASE + fi + + mv $LUAJIT_BASE/* . + rm -r $LUAJIT_BASE + + if [ "$LUA" == "luajit2.1" ]; then + git checkout v2.1 + fi + + make && cd src && ln -s luajit lua + fi +else + if [ "$LUA" == "lua5.1" ]; then + curl http://www.lua.org/ftp/lua-5.1.5.tar.gz | tar xz + mv lua-5.1.5 $LUA_DIR + elif [ "$LUA" == "lua5.2" ]; then + curl http://www.lua.org/ftp/lua-5.2.3.tar.gz | tar xz + mv lua-5.2.3 $LUA_DIR + elif [ "$LUA" == "lua5.3" ]; then + curl http://www.lua.org/ftp/lua-5.3.0.tar.gz | tar xz + mv lua-5.3.0 $LUA_DIR + fi + + cd $LUA_DIR + make $PLATFORM +fi + +########## +# Luarocks +########## + +LUAROCKS_BASE=luarocks-$LUAROCKS_VERSION +CONFIGURE_FLAGS="" + +cd $HOME +curl http://luarocks.org/releases/$LUAROCKS_BASE.tar.gz | tar xz +git clone https://github.com/keplerproject/luarocks.git $LUAROCKS_BASE + +mv $LUAROCKS_BASE $LUAROCKS_DIR +cd $LUAROCKS_DIR +git checkout v$LUAROCKS_VERSION + +if [ "$LUAJIT" == "yes" ]; then + LUA_DIR=$LUAJIT_DIR +elif [ "$LUA" == "lua5.1" ]; then + CONFIGURE_FLAGS=$CONFIGURE_FLAGS" --lua-version=5.1" +elif [ "$LUA" == "lua5.2" ]; then + CONFIGURE_FLAGS=$CONFIGURE_FLAGS" --lua-version=5.2" +elif [ "$LUA" == "lua5.3" ]; then + CONFIGURE_FLAGS=$CONFIGURE_FLAGS" --lua-version=5.3" +fi + +ls $LUA_DIR/src + +./configure \ + --prefix=$LUAROCKS_DIR \ + --with-lua-bin=$LUA_DIR/src \ + --with-lua-include=$LUA_DIR/src \ + $CONFIGURE_FLAGS + +make build && make install diff --git a/.travis.yml b/.travis.yml index 1a45d14..11316df 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,16 @@ language: erlang +sudo: false + env: global: - - LUAROCKS=2.2.2 - - CASSANDRA_VERSION=2.1.8 + - CASSANDRA_VERSION=2.1.9 + - LUAROCKS_VERSION=2.2.2 + - LUA_DIR=$HOME/lua + - LUAJIT_DIR=$HOME/luajit + - LUAROCKS_DIR=$HOME/luarocks + - "HOSTS=127.0.0.1,127.0.0.2,127.0.0.3" + - SMALL_LOAD=true matrix: - LUA=lua5.1 - LUA=lua5.2 @@ -11,16 +18,25 @@ env: - LUA=luajit before_install: - - bash .travis/setup_lua.sh - - bash .travis/setup_cassandra.sh - - sudo luarocks install luasocket - - sudo make dev + - bash .ci/setup_cassandra.sh + - bash .ci/setup_lua.sh + - export PATH=$LUA_DIR/src:$PATH + - export PATH=$LUAJIT_DIR/src:$PATH + - export PATH=$LUAROCKS_DIR/bin:$PATH + - luarocks install ansicolors + - make dev script: - - "busted -v --coverage" - - "make lint" + - busted -v --coverage -o .ci/busted_print.lua + - make lint + +after_success: luacov-coveralls -i cassandra -after_success: "luacov-coveralls -i cassandra" +cache: + directories: + - $LUAJIT_DIR + - $HOME/.ccm/repository/ # ccm cassandra version + - $HOME/.local/lib # python packages for ccm notifications: email: false diff --git a/.travis/setup_cassandra.sh b/.travis/setup_cassandra.sh deleted file mode 100755 index ae31144..0000000 --- a/.travis/setup_cassandra.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -CASSANDRA_BASE=apache-cassandra-$CASSANDRA_VERSION - -sudo rm -rf /var/lib/cassandra/* -curl http://apache.spinellicreations.com/cassandra/$CASSANDRA_VERSION/$CASSANDRA_BASE-bin.tar.gz | tar xz -sudo sh $CASSANDRA_BASE/bin/cassandra diff --git a/.travis/setup_lua.sh b/.travis/setup_lua.sh deleted file mode 100755 index e37d6cf..0000000 --- a/.travis/setup_lua.sh +++ /dev/null @@ -1,101 +0,0 @@ -#! /bin/bash - -# A script for setting up environment for travis-ci testing. -# Sets up Lua and Luarocks. -# LUA must be "lua5.1", "lua5.2" or "luajit". -# luajit2.0 - master v2.0 -# luajit2.1 - master v2.1 - -LUAJIT_BASE="LuaJIT-2.0.3" - -source .travis/platform.sh - -LUAJIT="no" - -if [ "$PLATFORM" == "macosx" ]; then - if [ "$LUA" == "luajit" ]; then - LUAJIT="yes"; - fi - if [ "$LUA" == "luajit2.0" ]; then - LUAJIT="yes"; - fi - if [ "$LUA" == "luajit2.1" ]; then - LUAJIT="yes"; - fi; -elif [ "$(expr substr $LUA 1 6)" == "luajit" ]; then - LUAJIT="yes"; -fi - -if [ "$LUAJIT" == "yes" ]; then - - if [ "$LUA" == "luajit" ]; then - curl http://luajit.org/download/$LUAJIT_BASE.tar.gz | tar xz; - else - git clone http://luajit.org/git/luajit-2.0.git $LUAJIT_BASE; - fi - - cd $LUAJIT_BASE - - if [ "$LUA" == "luajit2.1" ]; then - git checkout v2.1; - fi - - make && sudo make install - - if [ "$LUA" == "luajit2.1" ]; then - sudo ln -s /usr/local/bin/luajit-2.1.0-alpha /usr/local/bin/luajit - sudo ln -s /usr/local/bin/luajit /usr/local/bin/lua; - else - sudo ln -s /usr/local/bin/luajit /usr/local/bin/lua; - fi; - -else - if [ "$LUA" == "lua5.1" ]; then - curl http://www.lua.org/ftp/lua-5.1.5.tar.gz | tar xz - cd lua-5.1.5; - elif [ "$LUA" == "lua5.2" ]; then - curl http://www.lua.org/ftp/lua-5.2.3.tar.gz | tar xz - cd lua-5.2.3; - elif [ "$LUA" == "lua5.3" ]; then - curl http://www.lua.org/ftp/lua-5.3.0.tar.gz | tar xz - cd lua-5.3.0; - fi - sudo make $PLATFORM install; -fi - -cd $TRAVIS_BUILD_DIR; - -LUAROCKS_BASE=luarocks-$LUAROCKS - -# curl http://luarocks.org/releases/$LUAROCKS_BASE.tar.gz | tar xz - -git clone https://github.com/keplerproject/luarocks.git $LUAROCKS_BASE -cd $LUAROCKS_BASE - -git checkout v$LUAROCKS - -if [ "$LUA" == "luajit" ]; then - ./configure --lua-suffix=jit --with-lua-include=/usr/local/include/luajit-2.0; -elif [ "$LUA" == "luajit2.0" ]; then - ./configure --lua-suffix=jit --with-lua-include=/usr/local/include/luajit-2.0; -elif [ "$LUA" == "luajit2.1" ]; then - ./configure --lua-suffix=jit --with-lua-include=/usr/local/include/luajit-2.1; -else - ./configure; -fi - -make build && sudo make install - -cd $TRAVIS_BUILD_DIR - -rm -rf $LUAROCKS_BASE - -if [ "$LUAJIT" == "yes" ]; then - rm -rf $LUAJIT_BASE; -elif [ "$LUA" == "lua5.1" ]; then - rm -rf lua-5.1.5; -elif [ "$LUA" == "lua5.2" ]; then - rm -rf lua-5.2.3; -elif [ "$LUA" == "lua5.3" ]; then - rm -rf lua-5.3.0; -fi diff --git a/Makefile b/Makefile index 598d832..33f20e9 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,11 @@ DEV_ROCKS=busted luacov luacov-coveralls luacheck ldoc -.PHONY: dev clean test coverage lint doc +.PHONY: instal dev clean test coverage lint doc -dev: +install: + @luarocks make lua-cassandra-*.rockspec + +dev: install @for rock in $(DEV_ROCKS) ; do \ if ! command -v $$rock &> /dev/null ; then \ echo $$rock not found, installing via luarocks... ; \ diff --git a/lua-cassandra-0.4.0-0.rockspec b/lua-cassandra-0.4.0-0.rockspec index cb33731..18468df 100644 --- a/lua-cassandra-0.4.0-0.rockspec +++ b/lua-cassandra-0.4.0-0.rockspec @@ -10,7 +10,7 @@ description = { license = "MIT" } dependencies = { - "luasocket ~> 2.0.2-6", + "luasocket ~> 3.0-rc1", "lua-cjson ~> 2.1.0-1" } build = { diff --git a/spec/integration/cassandra_spec.lua b/spec/integration/cassandra_spec.lua index ceb03e1..7ee8bc3 100644 --- a/spec/integration/cassandra_spec.lua +++ b/spec/integration/cassandra_spec.lua @@ -12,20 +12,21 @@ local LOG_LVL = "ERR" utils.set_log_lvl(LOG_LVL) local _shm = "cassandra_specs" +local _hosts = utils.hosts describe("spawn cluster", function() it("should require a 'shm' option", function() assert.has_error(function() cassandra.spawn_cluster({ shm = nil, - contact_points = utils.contact_points + contact_points = _hosts }) end, "shm is required for spawning a cluster/session") end) it("should spawn a cluster", function() local ok, err = cassandra.spawn_cluster({ shm = _shm, - contact_points = utils.contact_points + contact_points = _hosts }) assert.falsy(err) assert.True(ok) @@ -35,7 +36,7 @@ describe("spawn cluster", function() local hosts, err = cache.get_hosts(_shm) assert.falsy(err) -- index of hosts - assert.equal(3, #hosts) + assert.equal(#_hosts, #hosts) -- hosts details for _, host_addr in ipairs(hosts) do local host_details = cache.get_host(_shm, host_addr) @@ -49,7 +50,7 @@ describe("spawn cluster", function() end) local contact_points = {"0.0.0.1", "0.0.0.2", "0.0.0.3"} - contact_points[#contact_points + 1] = utils.contact_points[1] + contact_points[#contact_points + 1] = _hosts[1] local ok, err = cassandra.spawn_cluster({ shm = "test", @@ -72,7 +73,6 @@ describe("spawn cluster", function() assert.truthy(err) assert.False(ok) assert.equal("NoHostAvailableError", err.type) - assert.equal("All hosts tried for query failed. 0.0.0.1: No route to host. 0.0.0.2: No route to host. 0.0.0.3: No route to host.", err.message) end) it("should accept a custom port for given hosts", function() utils.set_log_lvl("QUIET") @@ -81,7 +81,7 @@ describe("spawn cluster", function() end) local contact_points = {} - for i, addr in ipairs(utils.contact_points) do + for i, addr in ipairs(_hosts) do contact_points[i] = addr..":9043" end local ok, err = cassandra.spawn_cluster({ @@ -101,7 +101,7 @@ describe("spawn cluster", function() local ok, err = cassandra.spawn_cluster({ shm = "test", protocol_options = {default_port = 9043}, - contact_points = utils.contact_points + contact_points = _hosts }) assert.truthy(err) assert.False(ok) @@ -110,7 +110,7 @@ describe("spawn cluster", function() it("should return a third parameter, cluster, an instance able to spawn sessions", function() local ok, err, cluster = cassandra.spawn_cluster({ shm = "test", - contact_points = utils.contact_points + contact_points = _hosts }) assert.falsy(err) assert.True(ok) @@ -273,7 +273,7 @@ describe("session", function() assert.False(rows.meta.has_more_pages) end) it("support somewhat heavier insertions", function() - for i = 2, 10000 do + for i = 2, utils.n_inserts do local res, err = session:execute("INSERT INTO users(id, name, n) VALUES(2644bada-852c-11e3-89fb-e0b9a54a6d93, ?, ?)", {"Alice", i}) assert.falsy(err) assert.truthy(res) @@ -282,18 +282,18 @@ describe("session", function() local rows, err = session:execute("SELECT COUNT(*) FROM users") assert.falsy(err) assert.truthy(rows) - assert.equal(10000, rows[1].count) + assert.equal(utils.n_inserts, rows[1].count) end) - it("should have a default page_size (5000)", function() + it("should have a default page_size (1000)", function() local rows, err = session:execute("SELECT * FROM users WHERE id = 2644bada-852c-11e3-89fb-e0b9a54a6d93 ORDER BY n") assert.falsy(err) assert.truthy(rows) assert.truthy(rows.meta) assert.True(rows.meta.has_more_pages) assert.truthy(rows.meta.paging_state) - assert.equal(5000, #rows) + assert.equal(1000, #rows) assert.equal(1, rows[1].n) - assert.equal(5000, rows[#rows].n) + assert.equal(1000, rows[#rows].n) end) it("should be possible to specify a per-query page_size option", function() local rows, err = session:execute("SELECT * FROM users WHERE id = 2644bada-852c-11e3-89fb-e0b9a54a6d93 ORDER BY n", nil, {page_size = 100}) @@ -304,7 +304,7 @@ describe("session", function() local rows, err = session:execute("SELECT * FROM users") assert.falsy(err) assert.truthy(rows) - assert.equal(5000, #rows) + assert.equal(1000, #rows) -- back to the default end) it("should support passing a paging_state to retrieve next pages", function() local rows, err = session:execute("SELECT * FROM users WHERE id = 2644bada-852c-11e3-89fb-e0b9a54a6d93 ORDER BY n", nil, {page_size = 100}) @@ -333,12 +333,12 @@ describe("session", function() assert.equal(10, #rows) end - assert.equal(1000, page_tracker) + assert.equal(utils.n_inserts/10, page_tracker) end) it("should return the latest page of a set", function() -- When the latest page contains only 1 element local page_tracker = 0 - for rows, err, page in session:execute("SELECT * FROM users", nil, {page_size = 9999, auto_paging = true}) do + for rows, err, page in session:execute("SELECT * FROM users", nil, {page_size = utils.n_inserts - 1, auto_paging = true}) do assert.falsy(err) page_tracker = page_tracker + 1 assert.equal(page_tracker, page) @@ -348,11 +348,11 @@ describe("session", function() -- Even if all results are fetched in the first page page_tracker = 0 - for rows, err, page in session:execute("SELECT * FROM users", nil, {page_size = 10000, auto_paging = true}) do + for rows, err, page in session:execute("SELECT * FROM users", nil, {page_size = utils.n_inserts, auto_paging = true}) do assert.falsy(err) page_tracker = page_tracker + 1 assert.equal(page_tracker, page) - assert.equal(10000, #rows) + assert.equal(utils.n_inserts, #rows) end assert.same(1, page_tracker) @@ -401,7 +401,7 @@ describe("session", function() assert.spy(cache.set_prepared_query_id).was.not_called() end) it("should support a heavier load of prepared queries", function() - for i = 1, 10000 do + for i = 1, utils.n_inserts do local rows, err = session:execute("SELECT * FROM users", nil, {prepare = false, page_size = 10}) assert.falsy(err) assert.truthy(rows) @@ -425,7 +425,7 @@ describe("session", function() page_tracker = page end - assert.equal(1000, page_tracker) + assert.equal(utils.n_inserts/10, page_tracker) assert.spy(cache.get_prepared_query_id).was.called(page_tracker + 1) assert.spy(cache.set_prepared_query_id).was.called(0) end) diff --git a/spec/integration/cql_types_spec.lua b/spec/integration/cql_types_spec.lua index f724355..26d50c3 100644 --- a/spec/integration/cql_types_spec.lua +++ b/spec/integration/cql_types_spec.lua @@ -2,7 +2,8 @@ local utils = require "spec.spec_utils" local cassandra = require "cassandra" local _shm = "cql_types" -local _KEYSPACE = "resty_cassandra_cql_types_specs" +local _hosts = utils.hosts +local _keyspace = "resty_cassandra_cql_types_specs" -- Define log level for tests utils.set_log_lvl("ERR") @@ -13,16 +14,16 @@ describe("CQL types integration", function() setup(function() local _, err = cassandra.spawn_cluster({ shm = _shm, - contact_points = utils.contact_points + contact_points = _hosts }) assert.falsy(err) session, err = cassandra.spawn_session({shm = _shm}) assert.falsy(err) - utils.create_keyspace(session, _KEYSPACE) + utils.create_keyspace(session, _keyspace) - _, err = session:set_keyspace(_KEYSPACE) + _, err = session:set_keyspace(_keyspace) assert.falsy(err) _, err = session:execute [[ @@ -65,7 +66,7 @@ describe("CQL types integration", function() end) teardown(function() - utils.drop_keyspace(session, _KEYSPACE) + utils.drop_keyspace(session, _keyspace) session:shutdown() end) diff --git a/spec/spec_utils.lua b/spec/spec_utils.lua index 256d746..ed306fc 100644 --- a/spec/spec_utils.lua +++ b/spec/spec_utils.lua @@ -2,6 +2,14 @@ local say = require "say" local log = require "cassandra.log" local types = require "cassandra.types" local assert = require "luassert.assert" +local string_utils = require "cassandra.utils.string" + +local unpack +if _VERSION == "Lua 5.3" then + unpack = table.unpack +else + unpack = _G.unpack +end local _M = {} @@ -127,6 +135,12 @@ _M.cql_tuple_fixtures = { {type = {"text", "text"}, value = {"world", "hello"}} } -_M.contact_points = {"127.0.0.1", "127.0.0.2"} +local HOSTS = os.getenv("HOSTS") +HOSTS = HOSTS and string_utils.split(HOSTS, ",") or {"127.0.0.1"} + +local SMALL_LOAD = os.getenv("SMALL_LOAD") ~= nil + +_M.hosts = HOSTS +_M.n_inserts = SMALL_LOAD and 1000 or 10000 return _M diff --git a/src/cassandra/options.lua b/src/cassandra/options.lua index 0ccae96..6371c7c 100644 --- a/src/cassandra/options.lua +++ b/src/cassandra/options.lua @@ -19,7 +19,7 @@ local DEFAULTS = { query_options = { consistency = types.consistencies.one, serial_consistency = types.consistencies.serial, - page_size = 5000, + page_size = 1000, paging_state = nil, auto_paging = false, prepare = false, diff --git a/src/cassandra/requests.lua b/src/cassandra/requests.lua index 83ca708..14cbf48 100644 --- a/src/cassandra/requests.lua +++ b/src/cassandra/requests.lua @@ -7,6 +7,12 @@ local FrameHeader = require "cassandra.types.frame_header" local OP_CODES = types.OP_CODES local string_byte = string.byte local string_format = string.format +local unpack +if _VERSION == "Lua 5.3" then + unpack = table.unpack +else + unpack = _G.unpack +end local CQL_VERSION = "3.0.0" From d41764f3a2ccded2f215e47544163c29eb7702b4 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Mon, 7 Dec 2015 18:29:30 -0800 Subject: [PATCH 57/78] chore(ci) openresty integration tests --- .ci/run_tests.sh | 7 ++++++ .ci/setup_lua.sh | 20 +++++++++++------- .ci/setup_openresty.sh | 44 ++++++++++++++++++++++++++++++++++++++ .travis.yml | 48 ++++++++++++++++++++++++------------------ t/01-cassandra.t | 8 ++++++- 5 files changed, 98 insertions(+), 29 deletions(-) create mode 100755 .ci/run_tests.sh create mode 100755 .ci/setup_openresty.sh diff --git a/.ci/run_tests.sh b/.ci/run_tests.sh new file mode 100755 index 0000000..4e276a5 --- /dev/null +++ b/.ci/run_tests.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +if [ "$OPENRESTY_TESTS" != "yes" ]; then + busted -v --coverage -o .ci/busted_print.lua && make lint && luacov-coveralls -i cassandra +else + prove -l t +fi diff --git a/.ci/setup_lua.sh b/.ci/setup_lua.sh index fc08986..cffae9a 100755 --- a/.ci/setup_lua.sh +++ b/.ci/setup_lua.sh @@ -1,4 +1,4 @@ -#! /bin/bash +#!/bin/bash # A script for setting up environment for travis-ci testing. # Sets up Lua and Luarocks. @@ -31,13 +31,13 @@ fi if [ "$LUAJIT" == "yes" ]; then + LUA_INCLUDE="$LUAJIT_DIR/include/luajit-2.0" mkdir -p $LUAJIT_DIR # If cache is empty, downlaod and compile if [ ! "$(ls -A $LUAJIT_DIR)" ]; then LUAJIT_BASE="LuaJIT-2.0.4" - cd $LUAJIT_DIR if [ "$LUA" == "luajit" ]; then curl http://luajit.org/download/$LUAJIT_BASE.tar.gz | tar xz @@ -45,16 +45,19 @@ if [ "$LUAJIT" == "yes" ]; then git clone http://luajit.org/git/luajit-2.0.git $LUAJIT_BASE fi - mv $LUAJIT_BASE/* . - rm -r $LUAJIT_BASE + pushd $LUAJIT_BASE if [ "$LUA" == "luajit2.1" ]; then git checkout v2.1 fi - make && cd src && ln -s luajit lua + make + make install PREFIX=$LUAJIT_DIR + ln -s $LUAJIT_DIR/bin/luajit $LUAJIT_DIR/bin/lua fi else + LUA_INCLUDE="$LUA_DIR/include" + if [ "$LUA" == "lua5.1" ]; then curl http://www.lua.org/ftp/lua-5.1.5.tar.gz | tar xz mv lua-5.1.5 $LUA_DIR @@ -68,6 +71,7 @@ else cd $LUA_DIR make $PLATFORM + make install INSTALL_TOP=$LUA_DIR fi ########## @@ -95,12 +99,12 @@ elif [ "$LUA" == "lua5.3" ]; then CONFIGURE_FLAGS=$CONFIGURE_FLAGS" --lua-version=5.3" fi -ls $LUA_DIR/src +tree $LUA_DIR ./configure \ --prefix=$LUAROCKS_DIR \ - --with-lua-bin=$LUA_DIR/src \ - --with-lua-include=$LUA_DIR/src \ + --with-lua-bin=$LUA_DIR/bin \ + --with-lua-include=$LUA_INCLUDE \ $CONFIGURE_FLAGS make build && make install diff --git a/.ci/setup_openresty.sh b/.ci/setup_openresty.sh new file mode 100755 index 0000000..e26b439 --- /dev/null +++ b/.ci/setup_openresty.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +if [ "$OPENRESTY_TESTS" != "yes" ]; then + exit + echo "Exiting, no openresty tests" +fi + +set -e + +cd $HOME +mkdir -p $OPENRESTY_DIR + +if [ ! "$(ls -A $OPENRESTY_DIR)" ]; then + OPENRESTY_BASE=ngx_openresty-$OPENRESTY_VERSION + + tree $LUAJIT_DIR + + curl https://openresty.org/download/$OPENRESTY_BASE.tar.gz | tar xz + pushd $OPENRESTY_BASE + ./configure \ + --prefix=$OPENRESTY_DIR \ + --without-http_coolkit_module \ + --without-lua_resty_dns \ + --without-lua_resty_lrucache \ + --without-lua_resty_upstream_healthcheck \ + --without-lua_resty_websocket \ + --without-lua_resty_upload \ + --without-lua_resty_string \ + --without-lua_resty_mysql \ + --without-lua_resty_redis \ + --without-http_redis_module \ + --without-http_redis2_module \ + --without-lua_redis_parser + + make + make install + cd $HOME +fi + +git clone git://github.com/travis-perl/helpers travis-perl-helpers +pushd travis-perl-helpers +source ./init +popd +cpan-install Test::Nginx::Socket diff --git a/.travis.yml b/.travis.yml index 11316df..d3c9e41 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,25 @@ -language: erlang - +language: perl +perl: "5.18" sudo: false - +notifications: + email: false +addons: + apt: + packages: + - libreadline-dev + - libncurses5-dev + - libpcre3-dev + - libssl-dev + - build-essential env: global: - CASSANDRA_VERSION=2.1.9 - LUAROCKS_VERSION=2.2.2 + - OPENRESTY_VERSION=1.9.3.1 - LUA_DIR=$HOME/lua - LUAJIT_DIR=$HOME/luajit - LUAROCKS_DIR=$HOME/luarocks + - OPENRESTY_DIR=$HOME/openresty - "HOSTS=127.0.0.1,127.0.0.2,127.0.0.3" - SMALL_LOAD=true matrix: @@ -16,27 +27,24 @@ env: - LUA=lua5.2 - LUA=lua5.3 - LUA=luajit - + - OPENRESTY_TESTS: "yes" + LUA: "luajit" before_install: - bash .ci/setup_cassandra.sh - bash .ci/setup_lua.sh - - export PATH=$LUA_DIR/src:$PATH - - export PATH=$LUAJIT_DIR/src:$PATH - - export PATH=$LUAROCKS_DIR/bin:$PATH - - luarocks install ansicolors + - bash .ci/setup_openresty.sh + - export PATH=$LUA_DIR/src:$LUAJIT_DIR/bin:$LUAROCKS_DIR/bin:$OPENRESTY_DIR/nginx/sbin:$PATH + - export LUA_PATH="./?.lua;$LUAROCKS_DIR/share/lua/5.1/?.lua;$LUAROCKS_DIR/share/lua/5.1/?/init.lua;$LUAROCKS_DIR/lib/lua/5.1/?.lua;$LUA_PATH" + - export LUA_CPATH="$LUAROCKS_DIR/lib/lua/5.1/?.so;$LUA_CPATH" +install: - make dev - -script: - - busted -v --coverage -o .ci/busted_print.lua - - make lint - -after_success: luacov-coveralls -i cassandra - + - luarocks install ansicolors +script: .ci/run_tests.sh cache: + cpan: true + apt: true + pip: true directories: - $LUAJIT_DIR - - $HOME/.ccm/repository/ # ccm cassandra version - - $HOME/.local/lib # python packages for ccm - -notifications: - email: false + - $OPENRESTY_DIR + - $HOME/.ccm/repository/ diff --git a/t/01-cassandra.t b/t/01-cassandra.t index ade2040..90d7cb6 100644 --- a/t/01-cassandra.t +++ b/t/01-cassandra.t @@ -141,7 +141,13 @@ local location /t { content_by_lua ' local cassandra = require "cassandra" - local session = cassandra.spawn_session {shm = "cassandra"} + local session = cassandra.spawn_session { + shm = "cassandra", + socket_options = { + connect_timeout = 5000, + read_timeout = 10000 + } + } local res, err = session:execute [[ CREATE KEYSPACE IF NOT EXISTS resty_t_keyspace WITH REPLICATION = {\'class\': \'SimpleStrategy\', \'replication_factor\': 1} From 60096df4117f2b24d9db122832af75a1f414ad80 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Tue, 8 Dec 2015 02:19:12 -0800 Subject: [PATCH 58/78] chore(ci) use installed luajit for OpenResty --- .ci/run_tests.sh | 6 +++- .ci/setup_lua.sh | 68 ++++++++++++++++++------------------------ .ci/setup_openresty.sh | 7 ++--- .travis.yml | 4 +-- Makefile | 2 +- 5 files changed, 40 insertions(+), 47 deletions(-) diff --git a/.ci/run_tests.sh b/.ci/run_tests.sh index 4e276a5..012db0d 100755 --- a/.ci/run_tests.sh +++ b/.ci/run_tests.sh @@ -1,7 +1,11 @@ #!/bin/bash +set -e + if [ "$OPENRESTY_TESTS" != "yes" ]; then - busted -v --coverage -o .ci/busted_print.lua && make lint && luacov-coveralls -i cassandra + busted -v --coverage -o .ci/busted_print.lua + make lint + luacov-coveralls -i cassandra else prove -l t fi diff --git a/.ci/setup_lua.sh b/.ci/setup_lua.sh index cffae9a..ed671de 100755 --- a/.ci/setup_lua.sh +++ b/.ci/setup_lua.sh @@ -9,69 +9,63 @@ LUAJIT="no" source .ci/platform.sh -cd $HOME ############ # Lua/LuaJIT ############ -if [ "$PLATFORM" == "macosx" ]; then - if [ "$LUA" == "luajit" ]; then - LUAJIT="yes" - fi - if [ "$LUA" == "luajit2.0" ]; then - LUAJIT="yes" - fi - if [ "$LUA" == "luajit2.1" ]; then - LUAJIT="yes" - fi -elif [ "$(expr substr $LUA 1 6)" == "luajit" ]; then +if [ "$LUA" == "luajit" ]; then + LUAJIT="yes" + LUA="luajit-2.0" +elif [ "$LUA" == "luajit-2.0" ]; then + LUAJIT="yes" +elif [ "$LUA" == "luajit-2.1" ]; then LUAJIT="yes" fi if [ "$LUAJIT" == "yes" ]; then - - LUA_INCLUDE="$LUAJIT_DIR/include/luajit-2.0" mkdir -p $LUAJIT_DIR - # If cache is empty, downlaod and compile + # If cache is empty, download and compile if [ ! "$(ls -A $LUAJIT_DIR)" ]; then + git clone http://luajit.org/git/luajit-2.0.git + pushd luajit-2.0 - LUAJIT_BASE="LuaJIT-2.0.4" - - if [ "$LUA" == "luajit" ]; then - curl http://luajit.org/download/$LUAJIT_BASE.tar.gz | tar xz - else - git clone http://luajit.org/git/luajit-2.0.git $LUAJIT_BASE - fi - - pushd $LUAJIT_BASE - - if [ "$LUA" == "luajit2.1" ]; then + if [ "$LUA" == "luajit-2.0" ]; then + git checkout v2.0.4 + elif [ "$LUA" == "luajit-2.1" ]; then git checkout v2.1 fi make make install PREFIX=$LUAJIT_DIR - ln -s $LUAJIT_DIR/bin/luajit $LUAJIT_DIR/bin/lua + popd + + if [ "$LUA" == "luajit-2.1" ]; then + ln -sf $LUAJIT_DIR/bin/luajit-2.1.0-beta1 $LUAJIT_DIR/bin/luajit + fi + + ln -sf $LUAJIT_DIR/bin/luajit $LUAJIT_DIR/bin/lua fi -else - LUA_INCLUDE="$LUA_DIR/include" + LUA_INCLUDE="$LUAJIT_DIR/include/$LUA" +else if [ "$LUA" == "lua5.1" ]; then curl http://www.lua.org/ftp/lua-5.1.5.tar.gz | tar xz - mv lua-5.1.5 $LUA_DIR + pushd lua-5.1.5 elif [ "$LUA" == "lua5.2" ]; then curl http://www.lua.org/ftp/lua-5.2.3.tar.gz | tar xz - mv lua-5.2.3 $LUA_DIR + pushd lua-5.2.3 elif [ "$LUA" == "lua5.3" ]; then curl http://www.lua.org/ftp/lua-5.3.0.tar.gz | tar xz - mv lua-5.3.0 $LUA_DIR + pushd lua-5.3.0 fi - cd $LUA_DIR make $PLATFORM make install INSTALL_TOP=$LUA_DIR + popd + + LUA_INCLUDE="$LUA_DIR/include" fi ########## @@ -81,12 +75,9 @@ fi LUAROCKS_BASE=luarocks-$LUAROCKS_VERSION CONFIGURE_FLAGS="" -cd $HOME -curl http://luarocks.org/releases/$LUAROCKS_BASE.tar.gz | tar xz git clone https://github.com/keplerproject/luarocks.git $LUAROCKS_BASE -mv $LUAROCKS_BASE $LUAROCKS_DIR -cd $LUAROCKS_DIR +pushd $LUAROCKS_BASE git checkout v$LUAROCKS_VERSION if [ "$LUAJIT" == "yes" ]; then @@ -99,8 +90,6 @@ elif [ "$LUA" == "lua5.3" ]; then CONFIGURE_FLAGS=$CONFIGURE_FLAGS" --lua-version=5.3" fi -tree $LUA_DIR - ./configure \ --prefix=$LUAROCKS_DIR \ --with-lua-bin=$LUA_DIR/bin \ @@ -108,3 +97,4 @@ tree $LUA_DIR $CONFIGURE_FLAGS make build && make install +popd diff --git a/.ci/setup_openresty.sh b/.ci/setup_openresty.sh index e26b439..0bff14e 100755 --- a/.ci/setup_openresty.sh +++ b/.ci/setup_openresty.sh @@ -7,18 +7,17 @@ fi set -e -cd $HOME mkdir -p $OPENRESTY_DIR if [ ! "$(ls -A $OPENRESTY_DIR)" ]; then OPENRESTY_BASE=ngx_openresty-$OPENRESTY_VERSION - tree $LUAJIT_DIR - curl https://openresty.org/download/$OPENRESTY_BASE.tar.gz | tar xz pushd $OPENRESTY_BASE + ./configure \ --prefix=$OPENRESTY_DIR \ + --with-luajit=$LUAJIT_DIR \ --without-http_coolkit_module \ --without-lua_resty_dns \ --without-lua_resty_lrucache \ @@ -34,7 +33,7 @@ if [ ! "$(ls -A $OPENRESTY_DIR)" ]; then make make install - cd $HOME + popd fi git clone git://github.com/travis-perl/helpers travis-perl-helpers diff --git a/.travis.yml b/.travis.yml index d3c9e41..1ce7527 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,9 +26,9 @@ env: - LUA=lua5.1 - LUA=lua5.2 - LUA=lua5.3 - - LUA=luajit + - LUA=luajit-2.1 - OPENRESTY_TESTS: "yes" - LUA: "luajit" + LUA: "luajit-2.1" before_install: - bash .ci/setup_cassandra.sh - bash .ci/setup_lua.sh diff --git a/Makefile b/Makefile index 33f20e9..d829544 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ coverage: clean @luacov cassandra lint: - @find . -not -path './doc/*' -name '*.lua' | xargs luacheck -q + @find src spec -not -path './doc/*' -name '*.lua' | xargs luacheck -q doc: @ldoc -c doc/config.ld src From 07d2ff6e8a3a069f0859622002c4df566440b05d Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Tue, 8 Dec 2015 14:19:46 -0800 Subject: [PATCH 59/78] fix(load balancing) handle corrupted shm + tests --- src/cassandra/policies/load_balancing.lua | 4 +- t/00-load-balancing-policies.t | 108 ++++++++++++++++++++++ 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/src/cassandra/policies/load_balancing.lua b/src/cassandra/policies/load_balancing.lua index a6acce5..127130c 100644 --- a/src/cassandra/policies/load_balancing.lua +++ b/src/cassandra/policies/load_balancing.lua @@ -16,7 +16,9 @@ return { local index, err = dict:incr("rr_index", 1) if err then - log.err("Cannot prepare shared round robin load balancing policy: "..err) + log.err("Cannot increment shared round robin load balancing policy index: "..err) + elseif index == nil then + index = 0 end local plan_index = math_fmod(index or 0, n) diff --git a/t/00-load-balancing-policies.t b/t/00-load-balancing-policies.t index 3b3d04c..61aa936 100644 --- a/t/00-load-balancing-policies.t +++ b/t/00-load-balancing-policies.t @@ -39,3 +39,111 @@ GET /t 127.0.0.3 --- no_error_log [error] + + + +=== TEST 2: multiple shared round robin +--- http_config eval +"$::HttpConfig" +--- config + location /t { + content_by_lua ' + local iter = require("cassandra.policies.load_balancing").SharedRoundRobin + local shm = "cassandra" + local hosts = {"127.0.0.1", "127.0.0.2", "127.0.0.3"} + + local iter1 = iter(shm, hosts) + local iter2 = iter(shm, hosts) + local iter3 = iter(shm, hosts) + + ngx.say(select(2, iter1())) + ngx.say(select(2, iter2())) + ngx.say(select(2, iter3())) + + ngx.say(select(2, iter1())) + ngx.say(select(2, iter1())) + + ngx.say(select(2, iter2())) + ngx.say(select(2, iter3())) + ngx.say(select(2, iter2())) + ngx.say(select(2, iter3())) + '; + } +--- request +GET /t +--- response_body +127.0.0.1 +127.0.0.2 +127.0.0.3 +127.0.0.2 +127.0.0.3 +127.0.0.3 +127.0.0.1 +127.0.0.1 +127.0.0.2 +--- no_error_log +[error] + + + +=== TEST 3: handling missing index in shm +--- http_config eval +"$::HttpConfig" +--- config + location /t { + content_by_lua ' + local iter = require("cassandra.policies.load_balancing").SharedRoundRobin + local shm = "cassandra" + local hosts = {"127.0.0.1", "127.0.0.2", "127.0.0.3"} + + local iter1 = iter(shm, hosts) + ngx.say(select(2, iter1())) + + local dict = ngx.shared[shm] + dict:delete("rr_index") + + iter1 = iter(shm, hosts) + ngx.say(select(2, iter1())) + '; + } +--- request +GET /t +--- response_body +127.0.0.1 +127.0.0.1 +--- no_error_log +[error] + + + +=== TEST 4: handling invalid index in shm +--- http_config eval +"$::HttpConfig" +--- config + location /t { + content_by_lua ' + local iter = require("cassandra.policies.load_balancing").SharedRoundRobin + local shm = "cassandra" + local hosts = {"127.0.0.1", "127.0.0.2", "127.0.0.3"} + + local iter1 = iter(shm, hosts) + ngx.say(select(2, iter1())) + + local dict = ngx.shared[shm] + local ok, err = dict:replace("rr_index", "hello") + if not ok then + ngx.say(err) + ngx.exit(500) + end + + iter1 = iter(shm, hosts) + ngx.say(select(2, iter1())) + '; + } +--- request +GET /t +--- response_body +127.0.0.1 +127.0.0.1 +--- error_log +Cannot increment shared round robin load balancing policy index: not a number From 36f04b070c0570a7b459250fd8ee419dd6ba92a1 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Tue, 8 Dec 2015 18:03:13 -0800 Subject: [PATCH 60/78] feat: refactor API (return obj, err), no more throws --- README.md | 8 ++- spec/integration/cassandra_spec.lua | 56 ++++++--------- spec/integration/cql_types_spec.lua | 6 +- spec/unit/errors_spec.lua | 87 +++++++++++++++++++++++ spec/unit/options_spec.lua | 85 +++++++++------------- src/cassandra.lua | 44 ++++++------ src/cassandra/cache.lua | 50 +++++++------ src/cassandra/errors.lua | 36 ++++++++-- src/cassandra/options.lua | 56 +++++++++++---- src/cassandra/policies/load_balancing.lua | 9 +-- src/cassandra/policies/reconnection.lua | 14 ++-- t/00-load-balancing-policies.t | 2 +- t/01-cassandra.t | 6 +- 13 files changed, 291 insertions(+), 168 deletions(-) create mode 100644 spec/unit/errors_spec.lua diff --git a/README.md b/README.md index 650fd1b..7d2518b 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ It is build on the model of the official Datastax drivers, and tries to implemen - Client authentication - Highly configurable options per session/request - Compatible with Cassandra 2.0 and 2.1 +- Works with Lua 5.1, 5.2, 5.3 and LuaJIT 2.x ## Usage @@ -32,11 +33,11 @@ http { local cassandra = require "cassandra" -- retrieve cluster topology - local ok, err = cassandra.spawn_cluster { + local cluster, err = cassandra.spawn_cluster { shm = "cassandra", -- defined by "lua_shared_dict" contact_points = {"127.0.0.1", "127.0.0.2"} } - if not ok then + if err then ngx.log(ngx.ERR, "Could not spawn cluster: ", err.message) end '; @@ -100,10 +101,11 @@ With plain Lua: ```lua local cassandra = require "cassandra" -local ok, err, cluster = cassandra.spawn_cluster { +local cluster, err = cassandra.spawn_cluster { shm = "cassandra", contact_points = {"127.0.0.1", "127.0.0.2"} } +assert(err == nil) local session, err = cluster:spawn_session() assert(err == nil) diff --git a/spec/integration/cassandra_spec.lua b/spec/integration/cassandra_spec.lua index 7ee8bc3..b69aa43 100644 --- a/spec/integration/cassandra_spec.lua +++ b/spec/integration/cassandra_spec.lua @@ -16,20 +16,20 @@ local _hosts = utils.hosts describe("spawn cluster", function() it("should require a 'shm' option", function() - assert.has_error(function() - cassandra.spawn_cluster({ - shm = nil, - contact_points = _hosts - }) - end, "shm is required for spawning a cluster/session") + local cluster, err = cassandra.spawn_cluster { + shm = nil, + contact_points = _hosts + } + assert.falsy(cluster) + assert.equal("shm is required for spawning a cluster/session", err) end) it("should spawn a cluster", function() - local ok, err = cassandra.spawn_cluster({ + local cluster, err = cassandra.spawn_cluster { shm = _shm, contact_points = _hosts - }) + } assert.falsy(err) - assert.True(ok) + assert.truthy(cluster) end) it("should retrieve cluster infos in spawned cluster's shm", function() local cache = require "cassandra.cache" @@ -52,12 +52,12 @@ describe("spawn cluster", function() local contact_points = {"0.0.0.1", "0.0.0.2", "0.0.0.3"} contact_points[#contact_points + 1] = _hosts[1] - local ok, err = cassandra.spawn_cluster({ + local cluster, err = cassandra.spawn_cluster({ shm = "test", contact_points = contact_points }) assert.falsy(err) - assert.True(ok) + assert.truthy(cluster) end) it("should return an error when no contact_point is valid", function() utils.set_log_lvl("QUIET") @@ -66,12 +66,12 @@ describe("spawn cluster", function() end) local contact_points = {"0.0.0.1", "0.0.0.2", "0.0.0.3"} - local ok, err = cassandra.spawn_cluster({ + local cluster, err = cassandra.spawn_cluster({ shm = "test", contact_points = contact_points }) assert.truthy(err) - assert.False(ok) + assert.falsy(cluster) assert.equal("NoHostAvailableError", err.type) end) it("should accept a custom port for given hosts", function() @@ -84,12 +84,12 @@ describe("spawn cluster", function() for i, addr in ipairs(_hosts) do contact_points[i] = addr..":9043" end - local ok, err = cassandra.spawn_cluster({ + local cluster, err = cassandra.spawn_cluster({ shm = "test", contact_points = contact_points }) assert.truthy(err) - assert.False(ok) + assert.falsy(cluster) assert.equal("NoHostAvailableError", err.type) end) it("should accept a custom port through an option", function() @@ -98,41 +98,27 @@ describe("spawn cluster", function() utils.set_log_lvl(LOG_LVL) end) - local ok, err = cassandra.spawn_cluster({ + local cluster, err = cassandra.spawn_cluster({ shm = "test", protocol_options = {default_port = 9043}, contact_points = _hosts }) assert.truthy(err) - assert.False(ok) + assert.falsy(cluster) assert.equal("NoHostAvailableError", err.type) end) - it("should return a third parameter, cluster, an instance able to spawn sessions", function() - local ok, err, cluster = cassandra.spawn_cluster({ - shm = "test", - contact_points = _hosts - }) - assert.falsy(err) - assert.True(ok) - assert.truthy(cluster) - assert.truthy(cluster.spawn_session) - end) end) describe("spawn session", function() local session it("should require a 'shm' option", function() - assert.has_error(function() - cassandra.spawn_session({ - shm = nil - }) - end, "shm is required for spawning a cluster/session") + local session, err = cassandra.spawn_session({shm = nil}) + assert.falsy(session) + assert.equal("shm is required for spawning a cluster/session", err) end) it("should spawn a session", function() local err - session, err = cassandra.spawn_session({ - shm = _shm - }) + session, err = cassandra.spawn_session({shm = _shm}) assert.falsy(err) assert.truthy(session) assert.truthy(session.hosts) diff --git a/spec/integration/cql_types_spec.lua b/spec/integration/cql_types_spec.lua index 26d50c3..9e4864f 100644 --- a/spec/integration/cql_types_spec.lua +++ b/spec/integration/cql_types_spec.lua @@ -12,18 +12,18 @@ describe("CQL types integration", function() local session setup(function() - local _, err = cassandra.spawn_cluster({ + local cluster, err = cassandra.spawn_cluster({ shm = _shm, contact_points = _hosts }) assert.falsy(err) - session, err = cassandra.spawn_session({shm = _shm}) + session, err = cluster:spawn_session({shm = _shm}) assert.falsy(err) utils.create_keyspace(session, _keyspace) - _, err = session:set_keyspace(_keyspace) + local _, err = session:set_keyspace(_keyspace) assert.falsy(err) _, err = session:execute [[ diff --git a/spec/unit/errors_spec.lua b/spec/unit/errors_spec.lua new file mode 100644 index 0000000..0e618e5 --- /dev/null +++ b/spec/unit/errors_spec.lua @@ -0,0 +1,87 @@ +_G.test = true +local errors = require "cassandra.errors" +local Errors = errors.errors +local error_mt = errors.error_mt +local build_err = errors.build_error + +describe("Error", function() + describe("build_error", function() + it("should return an object with error_mt metatable", function() + local Err = build_err("fixture", {info = "foo"}) + assert.equal("function", type(Err)) + + local some_err = Err("bar") + assert.same(error_mt, getmetatable(some_err)) + assert.equal("foo", some_err.info) + assert.equal("fixture", some_err.type) + assert.equal("bar", some_err.message) + end) + it("should attach additional values through meta", function() + local Err = build_err("fixture", { + info = "fixture error", + meta = function(foo) + return {foo = foo} + end + }) + + local some_err = Err("bar") + assert.equal("bar", some_err.foo) + end) + end) + describe("error_mt", function() + local Err = build_err("fixture", {info = "foo"}) + + it("should have a __tostring metamethod", function() + local some_err = Err("bar") + assert.has_no_error(function() + tostring(some_err) + end) + assert.equal("fixture: bar", tostring(some_err)) + end) + it("should have a __concat metamethod", function() + local some_err = Err("bar") + assert.has_no_error(function() + tostring(some_err.." test") + tostring("test "..some_err) + end) + assert.equal("test fixture: bar", "test "..some_err) + assert.equal("fixture: bar test", some_err.." test") + end) + end) + describe("NoHostAvailableError", function() + it("should accept a string message", function() + local err = Errors.NoHostAvailableError("Nothing worked as planned") + assert.equal("NoHostAvailableError", err.type) + assert.equal("Nothing worked as planned", err.message) + end) + it("should accept a table", function() + local err = Errors.NoHostAvailableError({["127.0.0.1"] = "DOWN", ["127.0.0.2"] = "DOWN"}) + assert.equal("NoHostAvailableError", err.type) + assert.equal("All hosts tried for query failed. 127.0.0.1: DOWN. 127.0.0.2: DOWN.", err.message) + end) + end) + describe("ResponseError", function() + it("should accept an error code", function() + local err = Errors.ResponseError(666, "big error", "nothing worked") + assert.equal(666, err.code) + assert.equal("[big error] nothing worked", err.message) + end) + end) + describe("TimeoutError", function() + it("should accept an error code", function() + local err = Errors.TimeoutError("127.0.0.1") + assert.equal("timeout for peer 127.0.0.1", err.message) + end) + end) + describe("SharedDictError", function() + it("should accept a string", function() + local err = Errors.SharedDictError("no memory") + assert.equal("no memory", err.message) + end) + it("should accept a second argument: shm name", function() + local err = Errors.SharedDictError("no memory", "dict_name") + assert.equal("dict_name", err.shm) + assert.equal("shared dict dict_name returned error: no memory", err.message) + end) + end) +end) diff --git a/spec/unit/options_spec.lua b/spec/unit/options_spec.lua index efb4073..8f1170d 100644 --- a/spec/unit/options_spec.lua +++ b/spec/unit/options_spec.lua @@ -3,73 +3,56 @@ local cassandra = require "cassandra" describe("options parsing", function() describe("spawn_cluster", function() it("should require shm", function() - assert.has_error(function() - cassandra.spawn_cluster() - end, "shm is required for spawning a cluster/session") + local err = select(2, cassandra.spawn_cluster()) + assert.equal("shm is required for spawning a cluster/session", err) - assert.has_error(function() - cassandra.spawn_cluster({shm = 123}) - end, "shm must be a string") + err = select(2, cassandra.spawn_cluster({shm = 123})) + assert.equal("shm must be a string", err) - assert.has_error(function() - cassandra.spawn_cluster({shm = ""}) - end, "shm must be a valid string") + err = select(2, cassandra.spawn_cluster({shm = ""})) + assert.equal("shm must be a valid string", err) end) it("should require contact_points", function() - assert.has_error(function() - cassandra.spawn_cluster({ - shm = "test" - }) - end, "contact_points option is required") + local err = select(2, cassandra.spawn_cluster({shm = "test"})) + assert.equal("contact_points option is required", err) - assert.has_error(function() - cassandra.spawn_cluster({ - shm = "test", - contact_points = {} - }) - end, "contact_points must contain at least one contact point") + err = select(2, cassandra.spawn_cluster({shm = "test", contact_points = {}})) + assert.equal("contact_points must contain at least one contact point", err) - assert.has_error(function() - cassandra.spawn_cluster({ - shm = "test", - contact_points = {foo = "bar"} - }) - end, "contact_points must be an array (integer-indexed table)") + err = select(2, cassandra.spawn_cluster({shm = "test", contact_points = {foo = "bar"}})) + assert.equal("contact_points must be an array (integer-indexed table)", err) end) end) describe("spawn_session", function() it("should require shm", function() - assert.has_error(function() - cassandra.spawn_session() - end, "shm is required for spawning a cluster/session") + local err = select(2, cassandra.spawn_session()) + assert.equal("shm is required for spawning a cluster/session", err) - assert.has_error(function() - cassandra.spawn_session({shm = 123}) - end, "shm must be a string") + err = select(2, cassandra.spawn_session({shm = 123})) + assert.equal("shm must be a string", err) - assert.has_error(function() - cassandra.spawn_session({shm = ""}) - end, "shm must be a valid string") + err = select(2, cassandra.spawn_session({shm = ""})) + assert.equal("shm must be a valid string", err) end) it("should validate protocol_options", function() - assert.has_error(function() - cassandra.spawn_session({ - shm = "test", - protocol_options = { - default_port = "" - } - }) - end, "protocol default_port must be a number") + local err = select(2, cassandra.spawn_session({ + shm = "test", + protocol_options = { + default_port = "" + } + })) + + assert.equal("protocol default_port must be a number", err) end) it("should validate policies", function() - assert.has_error(function() - cassandra.spawn_session({ - shm = "test", - policies = { - address_resolution = "" - } - }) - end, "address_resolution policy must be a function") + local err = select(2, cassandra.spawn_session({ + shm = "test", + policies = { + address_resolution = "" + } + })) + + assert.equal("address_resolution policy must be a function", err) end) end) end) diff --git a/src/cassandra.lua b/src/cassandra.lua index ed2ce24..cb2d9ce 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -207,7 +207,7 @@ local function do_ssl_handshake(self) return false, err end else - -- returns a boolean since`reused_session` is false. + -- returns a boolean since `reused_session` is false. return self.socket:sslhandshake(false, nil, self.options.ssl_options.verify) end @@ -239,13 +239,13 @@ function Host:connect() local ok, err = self.socket:connect(self.host, self.port) if ok ~= 1 then --log.err("Could not connect to "..self.address..". Reason: "..err) - return false, err, true + return false, Errors.SocketError(self.address, err), true end if self.options.ssl_options ~= nil then ok, err = do_ssl_handshake(self) if not ok then - return false, err + return false, Errors.SocketError(self.address, err) end end @@ -352,7 +352,7 @@ function Host:set_keep_alive() local ok, err = self.socket:setkeepalive() if err then log.err("Could not set keepalive socket to "..self.address..". "..err) - return ok, err + return ok, Errors.SocketError(self.address, err) end end @@ -370,7 +370,7 @@ function Host:close() local _, err = self.socket:close() if err then log.err("Could not close socket to "..self.address..". "..err) - return false, err + return false, Errors.SocketError(self.address, err) end self.connected = false @@ -558,7 +558,7 @@ function RequestHandler:send_on_next_coordinator(request) return nil, err end - log.debug("Acquired connection through load balancing policy: "..coordinator.address) + log.info("Acquired connection through load balancing policy: "..coordinator.address) return self:send(request) end @@ -680,20 +680,23 @@ end local Session = {} function Session:new(options) - options = opts.parse_session(options) + local session_options, err = opts.parse_session(options) + if err then + return nil, err + end local s = { - options = options, + options = session_options, hosts = {} } - local host_addresses, cache_err = cache.get_hosts(options.shm) + local host_addresses, cache_err = cache.get_hosts(session_options.shm) if cache_err then return nil, cache_err end for _, addr in ipairs(host_addresses) do - table_insert(s.hosts, Host:new(addr, options)) + table_insert(s.hosts, Host:new(addr, session_options)) end return setmetatable(s, {__index = self}) @@ -792,7 +795,7 @@ end function Session:execute(query, args, query_options) if self.terminated then - return nil, Errors.NoHostAvailableError(nil, "Cannot reuse a session that has been shut down.") + return nil, Errors.NoHostAvailableError("Cannot reuse a session that has been shut down.") end local options = table_utils.deep_copy(self.options) @@ -876,7 +879,7 @@ end local SELECT_PEERS_QUERY = "SELECT peer,data_center,rack,rpc_address,release_version FROM system.peers" local SELECT_LOCAL_QUERY = "SELECT data_center,rack,rpc_address,release_version FROM system.local WHERE key='local'" ---- Retrieve cluster informations form a connected contact_point +--- Retrieve cluster informations from a connected contact_point function Cassandra.refresh_hosts(contact_points_hosts, options) log.info("Refreshing local and peers info") @@ -951,21 +954,22 @@ end --- Retrieve cluster informations and store them in ngx.shared.DICT function Cassandra.spawn_cluster(options) - options = opts.parse_cluster(options) + local cluster_options, err = opts.parse_cluster(options) + if err then + return nil, err + end local contact_points_hosts = {} - for _, contact_point in ipairs(options.contact_points) do - table_insert(contact_points_hosts, Host:new(contact_point, options)) + for _, contact_point in ipairs(cluster_options.contact_points) do + table_insert(contact_points_hosts, Host:new(contact_point, cluster_options)) end - local ok, err = Cassandra.refresh_hosts(contact_points_hosts, options) + local ok, err = Cassandra.refresh_hosts(contact_points_hosts, cluster_options) if not ok then - return false, err + return nil, err end - return true, nil, setmetatable({ - options = options - }, Cluster) + return setmetatable({options = cluster_options}, Cluster) end --- Cassandra Misc diff --git a/src/cassandra/cache.lua b/src/cassandra/cache.lua index 32f1215..d79deba 100644 --- a/src/cassandra/cache.lua +++ b/src/cassandra/cache.lua @@ -1,9 +1,15 @@ local log = require "cassandra.log" local json = require "cjson" +local Errors = require "cassandra.errors" local string_utils = require "cassandra.utils.string" local table_concat = table.concat local in_ngx = ngx ~= nil local shared +if in_ngx then + shared = ngx.shared +else + shared = {} +end -- DICT Proxy -- https://github.com/bsm/fakengx/blob/master/fakengx.lua @@ -90,12 +96,6 @@ function SharedDict:flush_expired(n) return flushed end -if in_ngx then - shared = ngx.shared -else - shared = {} -end - local function get_dict(shm) if not in_ngx then if shared[shm] == nil then @@ -103,7 +103,11 @@ local function get_dict(shm) end end - return shared[shm] + local dict = shared[shm] + if dict == nil then + error("No shared dict named "..shm) + end + return dict end --- Hosts @@ -116,21 +120,21 @@ local function set_hosts(shm, hosts) local dict = get_dict(shm) local ok, err = dict:safe_set(_HOSTS_KEY, table_concat(hosts, _SEP)) if not ok then - err = "Cannot store hosts for cluster under shm "..shm..": "..err + return false, Errors.SharedDictError("Cannot store hosts for cluster under shm "..shm..": "..err, shm) end - return ok, err + return true end local function get_hosts(shm) local dict = get_dict(shm) - local value, err = dict:get(_HOSTS_KEY) + local host_addresses, err = dict:get(_HOSTS_KEY) if err then - return nil, "Cannot retrieve hosts for cluster under shm "..shm..": "..err - elseif value == nil then - return nil, "Not hosts set for cluster under "..shm + return nil, Errors.SharedDictError(err, "Cannot retrieve hosts for cluster under shm "..shm..": "..err, shm) + elseif host_addresses == nil then + return nil, Errors.DriverError("No hosts set for cluster under shm: "..shm..". Is the cluster initialized?") end - return string_utils.split(value, _SEP) + return string_utils.split(host_addresses, _SEP) end --- Host @@ -140,18 +144,18 @@ local function set_host(shm, host_addr, host) local dict = get_dict(shm) local ok, err = dict:safe_set(host_addr, json.encode(host)) if not ok then - err = "Cannot store host details for cluster "..shm..": "..err + return false, Errors.SharedDictError("Cannot store host details for cluster "..shm..": "..err, shm) end - return ok, err + return true end local function get_host(shm, host_addr) local dict = get_dict(shm) local value, err = dict:get(host_addr) if err then - return nil, "Cannot retrieve host details for cluster under shm "..shm..": "..err + return nil, Errors.SharedDictError("Cannot retrieve host details for cluster under shm "..shm..": "..err, shm) elseif value == nil then - return nil, "No details for host "..host_addr.." under shm "..shm + return nil, Errors.DriverError("No details for host "..host_addr.." under shm "..shm) end return json.decode(value) end @@ -170,12 +174,12 @@ local function set_prepared_query_id(options, query, query_id) local ok, err, forcible = dict:set(prepared_key, query_id) if not ok then - err = "Cannot store prepared query id in shm "..shm..": "..err + return false, Errors.SharedDictError("Cannot store prepared query id in shm "..shm..": "..err, shm) elseif forcible then log.warn("shm for prepared queries '"..shm.."' is running out of memory. Consider increasing its size.") - dict:flush_expired(1) + dict:flush_expired(1) -- flush oldest query end - return ok, err + return true end local function get_prepared_query_id(options, query) @@ -185,9 +189,9 @@ local function get_prepared_query_id(options, query) local value, err = dict:get(prepared_key) if err then - err = "Cannot retrieve prepared query id in shm "..shm..": "..err + return nil, Errors.SharedDictError("Cannot retrieve prepared query id in shm "..shm..": "..err, shm) end - return value, err, prepared_key + return value, nil, prepared_key end return { diff --git a/src/cassandra/errors.lua b/src/cassandra/errors.lua index 92fe09b..05a97c3 100644 --- a/src/cassandra/errors.lua +++ b/src/cassandra/errors.lua @@ -15,7 +15,7 @@ local ERROR_TYPES = { local message = "All hosts tried for query failed." for address, err in pairs(errors) do - message = string_format("%s %s: %s.", message, address, err) + message = string_format("%s %s: %s.", message, address, tostring(err)) end return message end @@ -43,6 +43,22 @@ local ERROR_TYPES = { }, AuthenticationError = { info = "Represents an authentication error from the driver or from a Cassandra node." + }, + SharedDictError = { + info = "Represents an error with the lua_shared_dict in use.", + message = function(msg, shm) + if shm ~= nil then + return "shared dict "..shm.." returned error: "..msg + else + return msg + end + end, + meta = function(message, shm) + return {shm = shm} + end + }, + DriverError = { + info = "Represents an error indicating the library is used in an erroneous way." } } @@ -69,8 +85,8 @@ end local _ERRORS = {} -for k, v in pairs(ERROR_TYPES) do - _ERRORS[k] = function(...) +local function build_error(k, v) + return function(...) local arg = {...} local err = { type = k, @@ -91,4 +107,16 @@ for k, v in pairs(ERROR_TYPES) do end end -return _ERRORS +for k, v in pairs(ERROR_TYPES) do + _ERRORS[k] = build_error(k, v) +end + +if _G.test then + return { + error_mt = _error_mt, + errors = _ERRORS, + build_error = build_error + } +else + return _ERRORS +end diff --git a/src/cassandra/options.lua b/src/cassandra/options.lua index 6371c7c..6267eef 100644 --- a/src/cassandra/options.lua +++ b/src/cassandra/options.lua @@ -1,5 +1,6 @@ local types = require "cassandra.types" local utils = require "cassandra.utils.table" +local type = type --- Defaults -- @section defaults @@ -43,46 +44,71 @@ local DEFAULTS = { -- } } -local function parse_session(options) +local function parse_session(options, lvl) if options == nil then options = {} end utils.extend_table(DEFAULTS, options) - if options.keyspace ~= nil then - assert(type(options.keyspace) == "string", "keyspace must be a string") + if options.keyspace ~= nil and type(options.keyspace) ~= "string" then + return nil, "keyspace must be a string" end - assert(options.shm ~= nil, "shm is required for spawning a cluster/session") - assert(type(options.shm) == "string", "shm must be a string") - assert(options.shm ~= "", "shm must be a valid string") + if options.shm == nil then + return nil, "shm is required for spawning a cluster/session" + end + + if type(options.shm) ~= "string" then + return nil, "shm must be a string" + end + + if options.shm == "" then + return nil, "shm must be a valid string" + end if options.prepared_shm == nil then options.prepared_shm = options.shm end - assert(type(options.prepared_shm) == "string", "prepared_shm must be a string") - assert(options.prepared_shm ~= "", "prepared_shm must be a valid string") + if type(options.prepared_shm) ~= "string" then + return nil, "prepared_shm must be a string" + end + + if options.prepared_shm == "" then + return nil, "prepared_shm must be a valid string" + end + + if type(options.protocol_options.default_port) ~= "number" then + return nil, "protocol default_port must be a number" + end - assert(type(options.protocol_options.default_port) == "number", "protocol default_port must be a number") - assert(type(options.policies.address_resolution) == "function", "address_resolution policy must be a function") + if type(options.policies.address_resolution) ~= "function" then + return nil, "address_resolution policy must be a function" + end return options end local function parse_cluster(options) - parse_session(options) + local err - assert(options.contact_points ~= nil, "contact_points option is required") + options, err = parse_session(options) + if err then + return nil, err + end + + if options.contact_points == nil then + return nil, "contact_points option is required" + end if type(options.contact_points) ~= "table" then - error("contact_points must be a table", 3) + return nil, "contact_points must be a table" end if not utils.is_array(options.contact_points) then - error("contact_points must be an array (integer-indexed table)") + return nil, "contact_points must be an array (integer-indexed table)" end if #options.contact_points < 1 then - error("contact_points must contain at least one contact point") + return nil, "contact_points must contain at least one contact point" end options.keyspace = nil -- it makes no sense to use keyspace in this context diff --git a/src/cassandra/policies/load_balancing.lua b/src/cassandra/policies/load_balancing.lua index 127130c..30c9421 100644 --- a/src/cassandra/policies/load_balancing.lua +++ b/src/cassandra/policies/load_balancing.lua @@ -1,5 +1,6 @@ local log = require "cassandra.log" local cache = require "cassandra.cache" +local index_key = "rr_index" local math_fmod = math.fmod return { @@ -9,14 +10,14 @@ return { local dict = cache.get_dict(shm) - local ok, err = dict:add("rr_index", -1) + local ok, err = dict:add(index_key, -1) if not ok and err ~= "exists" then - log.err("Cannot prepare shared round robin load balancing policy: "..err) + log.err("Cannot prepare shared round robin load balancing policy in shared dict "..shm..": "..err) end - local index, err = dict:incr("rr_index", 1) + local index, err = dict:incr(index_key, 1) if err then - log.err("Cannot increment shared round robin load balancing policy index: "..err) + log.err("Cannot increment shared round robin load balancing policy index in shared dict "..shm..": "..err) elseif index == nil then index = 0 end diff --git a/src/cassandra/policies/reconnection.lua b/src/cassandra/policies/reconnection.lua index 9f9ad44..19f042c 100644 --- a/src/cassandra/policies/reconnection.lua +++ b/src/cassandra/policies/reconnection.lua @@ -15,27 +15,29 @@ end local function shared_exponential_reconnection_policy(base_delay, max_delay) return { new_schedule = function(host) + local shm = host.options.shm local index_key = "exp_reconnection_idx_"..host.address - local dict = cache.get_dict(host.options.shm) + local dict = cache.get_dict(shm) local ok, err = dict:set(index_key, 0) if not ok then - log.err("Cannot reset schedule for shared exponential reconnection policy: "..err) + log.err("Cannot reset schedule for shared exponential reconnection policy in shared dict "..shm..": "..err) end end, next = function(host) + local shm = host.options.shm local index_key = "exp_reconnection_idx_"..host.address local dict = cache.get_dict(host.options.shm) local ok, err = dict:add(index_key, 0) if not ok and err ~= "exists" then - log.err("Cannot prepare shared exponential reconnection policy: "..err) + log.err("Cannot prepare shared exponential reconnection policy in shared dict "..shm..": "..err) end + local index = dict:incr(index_key, 1) + local delay - dict:incr(index_key, 1) - local index = dict:get(index_key) - if index > 64 then + if index == nil or index > 64 then delay = max_delay else delay = math_min(math_pow(index, 2) * base_delay, max_delay) diff --git a/t/00-load-balancing-policies.t b/t/00-load-balancing-policies.t index 61aa936..383e74a 100644 --- a/t/00-load-balancing-policies.t +++ b/t/00-load-balancing-policies.t @@ -146,4 +146,4 @@ GET /t 127.0.0.1 127.0.0.1 --- error_log -Cannot increment shared round robin load balancing policy index: not a number +Cannot increment shared round robin load balancing policy index in shared dict cassandra: not a number diff --git a/t/01-cassandra.t b/t/01-cassandra.t index 90d7cb6..892c16c 100644 --- a/t/01-cassandra.t +++ b/t/01-cassandra.t @@ -16,11 +16,11 @@ our $SpawnCluster = <<_EOC_; lua_shared_dict cassandra_prepared 1m; init_by_lua ' local cassandra = require "cassandra" - local ok, err = cassandra.spawn_cluster({ + local cluster, err = cassandra.spawn_cluster { shm = "cassandra", contact_points = {"127.0.0.1", "127.0.0.2"} - }) - if not ok then + } + if err then ngx.log(ngx.ERR, tostring(err)) end '; From cbb75d033b9c1da6b78036d005e5bc517ea5abbc Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Tue, 8 Dec 2015 20:12:41 -0800 Subject: [PATCH 61/78] specs: more test and group error handling tests --- spec/integration/cassandra_spec.lua | 40 ++------- spec/integration/error_handling_spec.lua | 103 +++++++++++++++++++++++ spec/unit/errors_spec.lua | 5 +- spec/unit/options_spec.lua | 99 ++++++++++++++++++---- src/cassandra.lua | 4 + src/cassandra/options.lua | 34 +++++++- 6 files changed, 230 insertions(+), 55 deletions(-) create mode 100644 spec/integration/error_handling_spec.lua diff --git a/spec/integration/cassandra_spec.lua b/spec/integration/cassandra_spec.lua index b69aa43..b295ece 100644 --- a/spec/integration/cassandra_spec.lua +++ b/spec/integration/cassandra_spec.lua @@ -14,15 +14,7 @@ utils.set_log_lvl(LOG_LVL) local _shm = "cassandra_specs" local _hosts = utils.hosts -describe("spawn cluster", function() - it("should require a 'shm' option", function() - local cluster, err = cassandra.spawn_cluster { - shm = nil, - contact_points = _hosts - } - assert.falsy(cluster) - assert.equal("shm is required for spawning a cluster/session", err) - end) +describe("spawn_cluster()", function() it("should spawn a cluster", function() local cluster, err = cassandra.spawn_cluster { shm = _shm, @@ -59,21 +51,6 @@ describe("spawn cluster", function() assert.falsy(err) assert.truthy(cluster) end) - it("should return an error when no contact_point is valid", function() - utils.set_log_lvl("QUIET") - finally(function() - utils.set_log_lvl(LOG_LVL) - end) - - local contact_points = {"0.0.0.1", "0.0.0.2", "0.0.0.3"} - local cluster, err = cassandra.spawn_cluster({ - shm = "test", - contact_points = contact_points - }) - assert.truthy(err) - assert.falsy(cluster) - assert.equal("NoHostAvailableError", err.type) - end) it("should accept a custom port for given hosts", function() utils.set_log_lvl("QUIET") finally(function() @@ -109,13 +86,8 @@ describe("spawn cluster", function() end) end) -describe("spawn session", function() +describe("spawn_session()", function() local session - it("should require a 'shm' option", function() - local session, err = cassandra.spawn_session({shm = nil}) - assert.falsy(session) - assert.equal("shm is required for spawning a cluster/session", err) - end) it("should spawn a session", function() local err session, err = cassandra.spawn_session({shm = _shm}) @@ -222,7 +194,7 @@ describe("session", function() session:shutdown() end) - describe(":set_keyspace()", function() + describe("set_keyspace()", function() it("should set a session's 'keyspace' option", function() local ok, err = session:set_keyspace(_KEYSPACE) assert.falsy(err) @@ -235,7 +207,7 @@ describe("session", function() end) end) - describe(":execute()", function() + describe("execute()", function() it("should accept values to bind", function() local res, err = session:execute("INSERT INTO users(id, name, n) VALUES(?, ?, ?)", {cassandra.uuid("2644bada-852c-11e3-89fb-e0b9a54a6d93"), "Bob", 1}) @@ -418,7 +390,7 @@ describe("session", function() end) end) - describe(":batch()", function() + describe("batch()", function() local _UUID = "ca002f0a-8fe4-11e5-9663-43d80ec97d3e" setup(function() @@ -564,7 +536,7 @@ describe("session", function() end) end) - describe(":shutdown()", function() + describe("shutdown()", function() it("should close all connection and make the session unusable", function() session:shutdown() assert.True(session.terminated) diff --git a/spec/integration/error_handling_spec.lua b/spec/integration/error_handling_spec.lua new file mode 100644 index 0000000..bebdf47 --- /dev/null +++ b/spec/integration/error_handling_spec.lua @@ -0,0 +1,103 @@ +local utils = require "spec.spec_utils" +local cassandra = require "cassandra" + +local LOG_LVL = "ERR" + +-- Define log level for tests +utils.set_log_lvl(LOG_LVL) + +local _shm = "cassandra_error_specs" +local _hosts = utils.hosts + +describe("error handling", function() + describe("spawn_cluster()", function() + it("should return option errors", function() + local options = require "cassandra.options" + spy.on(options, "parse_cluster") + finally(function() + options.parse_cluster:revert() + end) + + local cluster, err = cassandra.spawn_cluster() + assert.falsy(cluster) + assert.spy(options.parse_cluster).was.called() + assert.equal("shm is required for spawning a cluster/session", err) + + cluster, err = cassandra.spawn_cluster {} + assert.falsy(cluster) + assert.equal("shm is required for spawning a cluster/session", err) + + cluster, err = cassandra.spawn_cluster {shm = ""} + assert.falsy(cluster) + assert.equal("shm must be a valid string", err) + end) + it("should return an error when no contact_point is valid", function() + utils.set_log_lvl("QUIET") + finally(function() + utils.set_log_lvl(LOG_LVL) + end) + + local contact_points = {"0.0.0.1", "0.0.0.2", "0.0.0.3"} + local cluster, err = cassandra.spawn_cluster { + shm = "test", + contact_points = contact_points + } + assert.truthy(err) + assert.falsy(cluster) + assert.equal("NoHostAvailableError", err.type) + end) + end) + describe("shorthand serializers", function() + it("should require the first argument (value)", function() + assert.has_error(cassandra.uuid, "argument #1 required for 'uuid' type shorthand") + assert.has_error(cassandra.map, "argument #1 required for 'map' type shorthand") + assert.has_error(cassandra.list, "argument #1 required for 'list' type shorthand") + assert.has_error(cassandra.timestamp, "argument #1 required for 'timestamp' type shorthand") + local trace = debug.traceback() + local match = string.find(trace, "stack traceback:\n\tspec/integration/error_handling_spec.lua", nil, true) + assert.equal(1, match) + end) + end) + describe("spawn_session()", function() + it("should return options errors", function() + local options = require "cassandra.options" + spy.on(options, "parse_session") + finally(function() + options.parse_session:revert() + end) + + local session, err = cassandra.spawn_session() + assert.falsy(session) + assert.spy(options.parse_session).was.called() + assert.equal("shm is required for spawning a cluster/session", err) + end) + end) + describe("execute()", function() + local session + + setup(function() + local cluster, err = cassandra.spawn_cluster { + shm = _shm, + contact_points = _hosts + } + assert.falsy(err) + + session, err = cluster:spawn_session {shm = _shm} + assert.falsy(err) + end) + teardown(function() + session:shutdown() + end) + it("should handle CQL errors", function() + local res, err = session:execute("CAN I HAZ CQL") + assert.falsy(res) + assert.truthy(err) + assert.equal("ResponseError", err.type) + + res, err = session:execute("SELECT * FROM system.local WHERE key = ?") + assert.falsy(res) + assert.truthy(err) + assert.equal("ResponseError", err.type) + end) + end) +end) diff --git a/spec/unit/errors_spec.lua b/spec/unit/errors_spec.lua index 0e618e5..eccf2f6 100644 --- a/spec/unit/errors_spec.lua +++ b/spec/unit/errors_spec.lua @@ -55,9 +55,10 @@ describe("Error", function() assert.equal("Nothing worked as planned", err.message) end) it("should accept a table", function() - local err = Errors.NoHostAvailableError({["127.0.0.1"] = "DOWN", ["127.0.0.2"] = "DOWN"}) + local err = Errors.NoHostAvailableError({["abc"] = "DOWN", ["def"] = "DOWN"}) assert.equal("NoHostAvailableError", err.type) - assert.equal("All hosts tried for query failed. 127.0.0.1: DOWN. 127.0.0.2: DOWN.", err.message) + -- can't be sure in which order will the table be iterated over + assert.truthy(string.match(err.message, "All hosts tried for query failed%. %l%l%l: DOWN%. %l%l%l: DOWN%.")) end) end) describe("ResponseError", function() diff --git a/spec/unit/options_spec.lua b/spec/unit/options_spec.lua index 8f1170d..f616808 100644 --- a/spec/unit/options_spec.lua +++ b/spec/unit/options_spec.lua @@ -1,58 +1,125 @@ -local cassandra = require "cassandra" +local options = require "cassandra.options" +local parse_cluster = options.parse_cluster +local parse_session = options.parse_session describe("options parsing", function() - describe("spawn_cluster", function() + describe("parse_cluster", function() it("should require shm", function() - local err = select(2, cassandra.spawn_cluster()) + local err = select(2, parse_cluster()) assert.equal("shm is required for spawning a cluster/session", err) - err = select(2, cassandra.spawn_cluster({shm = 123})) + err = select(2, parse_cluster({shm = 123})) assert.equal("shm must be a string", err) - err = select(2, cassandra.spawn_cluster({shm = ""})) + err = select(2, parse_cluster({shm = ""})) assert.equal("shm must be a valid string", err) end) it("should require contact_points", function() - local err = select(2, cassandra.spawn_cluster({shm = "test"})) + local err = select(2, parse_cluster({shm = "test"})) assert.equal("contact_points option is required", err) - err = select(2, cassandra.spawn_cluster({shm = "test", contact_points = {}})) + err = select(2, parse_cluster({shm = "test", contact_points = {}})) assert.equal("contact_points must contain at least one contact point", err) - err = select(2, cassandra.spawn_cluster({shm = "test", contact_points = {foo = "bar"}})) + err = select(2, parse_cluster({shm = "test", contact_points = {foo = "bar"}})) assert.equal("contact_points must be an array (integer-indexed table)", err) end) + it("should ignore `keyspace` if given", function() + local options, err = parse_cluster { + shm = "test", + contact_points = {"127.0.0.1"}, + keyspace = "foo" + } + assert.falsy(err) + assert.falsy(options.keyspace) + end) end) - describe("spawn_session", function() + describe("parse_session", function() it("should require shm", function() - local err = select(2, cassandra.spawn_session()) + local err = select(2, parse_session()) assert.equal("shm is required for spawning a cluster/session", err) - err = select(2, cassandra.spawn_session({shm = 123})) + err = select(2, parse_session({shm = 123})) assert.equal("shm must be a string", err) - err = select(2, cassandra.spawn_session({shm = ""})) + err = select(2, parse_session({shm = ""})) assert.equal("shm must be a valid string", err) end) + it("should validate keyspace if given", function() + local err = select(2, parse_session({shm = "test", keyspace = 123})) + assert.equal("keyspace must be a valid string", err) + + err = select(2, parse_session({shm = "test", keyspace = ""})) + assert.equal("keyspace must be a valid string", err) + end) it("should validate protocol_options", function() - local err = select(2, cassandra.spawn_session({ + local err = select(2, parse_session({ shm = "test", protocol_options = { default_port = "" } })) - assert.equal("protocol default_port must be a number", err) + + err = select(2, parse_session({ + shm = "test", + protocol_options = { + max_schema_consensus_wait = "" + } + })) + assert.equal("protocol max_schema_consensus_wait must be a number", err) end) it("should validate policies", function() - local err = select(2, cassandra.spawn_session({ + local err = select(2, parse_session({ shm = "test", policies = { address_resolution = "" } })) - assert.equal("address_resolution policy must be a function", err) + + -- @TODO + -- validate other policies (need to freeze the API) + end) + it("should validate query options", function() + local err = select(2, parse_session({ + shm = "test", + query_options = { + page_size = "" + } + })) + assert.equal("query page_size must be a number", err) + end) + it("should validate socket options", function() + local err = select(2, parse_session({ + shm = "test", + socket_options = { + connect_timeout = "" + } + })) + assert.equal("socket connect_timeout must be a number", err) + + err = select(2, parse_session({ + shm = "test", + socket_options = { + read_timeout = "" + } + })) + assert.equal("socket read_timeout must be a number", err) + end) + it("should set `prepared_shm` to `shm` if nil", function() + local options, err = parse_session { + shm = "test" + } + assert.falsy(err) + assert.equal("test", options.prepared_shm) + + options, err = parse_session { + shm = "test", + prepared_shm = "prepared_test" + } + assert.falsy(err) + assert.equal("prepared_test", options.prepared_shm) end) end) end) diff --git a/src/cassandra.lua b/src/cassandra.lua index cb2d9ce..6315dba 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -982,6 +982,10 @@ local types_mt = {} function types_mt:__index(key) if CQL_TYPES[key] ~= nil then return function(value) + if value == nil then + error("argument #1 required for '"..key.."' type shorthand", 2) + end + return {value = value, type_id = CQL_TYPES[key]} end elseif key == "unset" then diff --git a/src/cassandra/options.lua b/src/cassandra/options.lua index 6267eef..e65bdc9 100644 --- a/src/cassandra/options.lua +++ b/src/cassandra/options.lua @@ -33,7 +33,7 @@ local DEFAULTS = { socket_options = { connect_timeout = 1000, read_timeout = 2000 - }, + } -- username = nil, -- password = nil, -- ssl_options = { @@ -48,10 +48,14 @@ local function parse_session(options, lvl) if options == nil then options = {} end utils.extend_table(DEFAULTS, options) - if options.keyspace ~= nil and type(options.keyspace) ~= "string" then - return nil, "keyspace must be a string" + -- keyspace + + if options.keyspace ~= nil and type(options.keyspace) ~= "string" or options.keyspace == "" then + return nil, "keyspace must be a valid string" end + -- shms + if options.shm == nil then return nil, "shm is required for spawning a cluster/session" end @@ -76,14 +80,38 @@ local function parse_session(options, lvl) return nil, "prepared_shm must be a valid string" end + -- protocol options + if type(options.protocol_options.default_port) ~= "number" then return nil, "protocol default_port must be a number" end + if type(options.protocol_options.max_schema_consensus_wait) ~= "number" then + return nil, "protocol max_schema_consensus_wait must be a number" + end + + -- policies + if type(options.policies.address_resolution) ~= "function" then return nil, "address_resolution policy must be a function" end + -- query options + + if type(options.query_options.page_size) ~= "number" then + return nil, "query page_size must be a number" + end + + -- socket options + + if type(options.socket_options.connect_timeout) ~= "number" then + return nil, "socket connect_timeout must be a number" + end + + if type(options.socket_options.read_timeout) ~= "number" then + return nil, "socket read_timeout must be a number" + end + return options end From f83070db2eae9c69878bbdcf1bd0cbc393a182e2 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Wed, 9 Dec 2015 14:13:08 -0800 Subject: [PATCH 62/78] feat(shm) refresh hosts if cluster infos disappeared from the shm --- spec/integration/error_handling_spec.lua | 29 +++++++ src/cassandra.lua | 55 ++++++++----- src/cassandra/cache.lua | 6 +- t/00-load-balancing-policies.t | 4 +- t/01-cassandra.t | 10 +-- t/02-error-handling.t | 98 ++++++++++++++++++++++++ 6 files changed, 172 insertions(+), 30 deletions(-) create mode 100644 t/02-error-handling.t diff --git a/spec/integration/error_handling_spec.lua b/spec/integration/error_handling_spec.lua index bebdf47..1a8d55e 100644 --- a/spec/integration/error_handling_spec.lua +++ b/spec/integration/error_handling_spec.lua @@ -100,4 +100,33 @@ describe("error handling", function() assert.equal("ResponseError", err.type) end) end) + describe("shm errors", function() + it("should trigger a cluster refresh if the hosts are not available anymore", function() + local shm = "test_shm_errors" + local cache = require "cassandra.cache" + local dict = cache.get_dict(shm) + assert.truthy(dict) + + local cluster, err = cassandra.spawn_cluster { + shm = shm, + contact_points = _hosts + } + assert.falsy(err) + assert.truthy(cache.get_hosts(shm)) + + -- erase hosts from the cache + dict:delete("hosts") + assert.falsy(cache.get_hosts(shm)) + + -- attempt session create + local session, err = cluster:spawn_session() + assert.falsy(err) + + -- attempt query + local rows, err = session:execute("SELECT * FROM system.local") + assert.falsy(err) + assert.truthy(rows) + assert.equal(1, #rows) + end) + end) end) diff --git a/src/cassandra.lua b/src/cassandra.lua index 6315dba..9ccce8a 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -57,6 +57,16 @@ end local MIN_PROTOCOL_VERSION = 2 local DEFAULT_PROTOCOL_VERSION = 3 +--- Cassandra +-- @section cassandra + +local Cassandra = { + _VERSION = "0.4.0", + DEFAULT_PROTOCOL_VERSION = DEFAULT_PROTOCOL_VERSION, + MIN_PROTOCOL_VERSION = MIN_PROTOCOL_VERSION +} + + --- Host -- A connection to a single host. -- Not cluster aware, only maintain a socket to its peer. @@ -693,6 +703,14 @@ function Session:new(options) local host_addresses, cache_err = cache.get_hosts(session_options.shm) if cache_err then return nil, cache_err + elseif host_addresses == nil then + log.warn("No cluster infos in shared dict '"..session_options.shm.."'.") + if session_options.contact_points ~= nil then + host_addresses, err = Cassandra.refresh_hosts(session_options) + if host_addresses == nil then + return nil, err + end + end end for _, addr in ipairs(host_addresses) do @@ -866,12 +884,6 @@ end --- Cassandra -- @section cassandra -local Cassandra = { - _VERSION = "0.4.0", - DEFAULT_PROTOCOL_VERSION = DEFAULT_PROTOCOL_VERSION, - MIN_PROTOCOL_VERSION = MIN_PROTOCOL_VERSION -} - function Cassandra.spawn_session(options) return Session:new(options) end @@ -880,12 +892,17 @@ local SELECT_PEERS_QUERY = "SELECT peer,data_center,rack,rpc_address,release_ver local SELECT_LOCAL_QUERY = "SELECT data_center,rack,rpc_address,release_version FROM system.local WHERE key='local'" --- Retrieve cluster informations from a connected contact_point -function Cassandra.refresh_hosts(contact_points_hosts, options) +function Cassandra.refresh_hosts(options) log.info("Refreshing local and peers info") + local contact_points_hosts = {} + for _, contact_point in ipairs(options.contact_points) do + table_insert(contact_points_hosts, Host:new(contact_point, options)) + end + local coordinator, err = RequestHandler.get_first_coordinator(contact_points_hosts) if err then - return false, err + return nil, err end local local_query = Requests.QueryRequest(SELECT_LOCAL_QUERY) @@ -894,7 +911,7 @@ function Cassandra.refresh_hosts(contact_points_hosts, options) local rows, err = coordinator:send(local_query) if err then - return false, err + return nil, err end local row = rows[1] local address = options.policies.address_resolution(row["rpc_address"]) @@ -911,7 +928,7 @@ function Cassandra.refresh_hosts(contact_points_hosts, options) rows, err = coordinator:send(peers_query) if err then - return false, err + return nil, err end for _, row in ipairs(rows) do @@ -937,11 +954,16 @@ function Cassandra.refresh_hosts(contact_points_hosts, options) table_insert(addresses, addr) local ok, cache_err = cache.set_host(options.shm, addr, host) if not ok then - return false, cache_err + return nil, cache_err end end - return cache.set_hosts(options.shm, addresses) + local ok, err = cache.set_hosts(options.shm, addresses) + if not ok then + return nil, err + end + + return addresses end local Cluster = {} @@ -959,13 +981,8 @@ function Cassandra.spawn_cluster(options) return nil, err end - local contact_points_hosts = {} - for _, contact_point in ipairs(cluster_options.contact_points) do - table_insert(contact_points_hosts, Host:new(contact_point, cluster_options)) - end - - local ok, err = Cassandra.refresh_hosts(contact_points_hosts, cluster_options) - if not ok then + local addresses, err = Cassandra.refresh_hosts(cluster_options) + if addresses == nil then return nil, err end diff --git a/src/cassandra/cache.lua b/src/cassandra/cache.lua index d79deba..67eaa6f 100644 --- a/src/cassandra/cache.lua +++ b/src/cassandra/cache.lua @@ -130,11 +130,9 @@ local function get_hosts(shm) local host_addresses, err = dict:get(_HOSTS_KEY) if err then return nil, Errors.SharedDictError(err, "Cannot retrieve hosts for cluster under shm "..shm..": "..err, shm) - elseif host_addresses == nil then - return nil, Errors.DriverError("No hosts set for cluster under shm: "..shm..". Is the cluster initialized?") + elseif host_addresses ~= nil then + return string_utils.split(host_addresses, _SEP) end - - return string_utils.split(host_addresses, _SEP) end --- Host diff --git a/t/00-load-balancing-policies.t b/t/00-load-balancing-policies.t index 383e74a..e33f997 100644 --- a/t/00-load-balancing-policies.t +++ b/t/00-load-balancing-policies.t @@ -145,5 +145,5 @@ GET /t --- response_body 127.0.0.1 127.0.0.1 ---- error_log -Cannot increment shared round robin load balancing policy index in shared dict cassandra: not a number +--- error_log eval +qr/\[error\].*?Cannot increment shared round robin load balancing policy index in shared dict cassandra: not a number/ diff --git a/t/01-cassandra.t b/t/01-cassandra.t index 892c16c..46e9918 100644 --- a/t/01-cassandra.t +++ b/t/01-cassandra.t @@ -18,7 +18,7 @@ our $SpawnCluster = <<_EOC_; local cassandra = require "cassandra" local cluster, err = cassandra.spawn_cluster { shm = "cassandra", - contact_points = {"127.0.0.1", "127.0.0.2"} + contact_points = {"127.0.0.1"} } if err then ngx.log(ngx.ERR, tostring(err)) @@ -203,7 +203,7 @@ GET /t local rows, err = session:execute("SELECT key FROM system.local") if err then - ngx.say(tostring(err)) + ngx.log(ngx.ERR, tostring(err)) return ngx.exit(200) end @@ -213,9 +213,9 @@ GET /t --- request GET /t --- response_body -NoHostAvailableError: Cannot reuse a session that has been shut down. ---- no_error_log -[error] + +--- error_log eval +qr/\[error\].*?NoHostAvailableError: Cannot reuse a session that has been shut down./ diff --git a/t/02-error-handling.t b/t/02-error-handling.t new file mode 100644 index 0000000..69ce63e --- /dev/null +++ b/t/02-error-handling.t @@ -0,0 +1,98 @@ +use Test::Nginx::Socket::Lua; +use Cwd qw(cwd); + +repeat_each(2); + +plan tests => repeat_each() * blocks() * 4; + +my $pwd = cwd(); + +our $HttpConfig = <<_EOC_; + lua_package_path "$pwd/src/?.lua;$pwd/src/?/init.lua;;"; +_EOC_ + +our $SpawnCluster = <<_EOC_; + lua_shared_dict cassandra 1m; + lua_shared_dict cassandra_prepared 1m; + init_by_lua ' + local cassandra = require "cassandra" + local cluster, err = cassandra.spawn_cluster { + shm = "cassandra", + contact_points = {"127.0.0.1"} + } + if err then + ngx.log(ngx.ERR, tostring(err)) + end + '; +_EOC_ + +run_tests(); + +__DATA__ + +=== TEST 1: shm cluster info disapeared +--- http_config eval +"$::HttpConfig + $::SpawnCluster" +--- config + location /t { + content_by_lua ' + local cassandra = require "cassandra" + local cache = require "cassandra.cache" + local shm = "cassandra" + + local dict = ngx.shared[shm] + local hosts, err = cache.get_hosts(shm) + if err then + ngx.log(ngx.ERR, tostring(err)) + ngx.exit(500) + elseif hosts == nil or #hosts < 1 then + ngx.log(ngx.ERR, "no hosts set in shm") + ngx.exit(500) + end + + -- erase hosts from the cache + dict:delete("hosts") + + local hosts, err = cache.get_hosts(shm) + if err then + ngx.log(ngx.ERR, tostring(err)) + ngx.exit(500) + elseif hosts ~= nil then + ngx.log(ngx.ERR, "hosts set in shm after delete") + ngx.exit(500) + end + + -- attempt to create session + local session, err = cassandra.spawn_session { + shm = shm, + contact_points = {"127.0.0.1"} -- safe contact point just in case + } + if err then + ngx.log(ngx.ERR, tostring(err)) + ngx.exit(500) + end + + -- attempt query + local rows, err = session:execute("SELECT * FROM system.local") + if err then + ngx.log(ngx.ERR, tostring(err)) + ngx.exit(500) + end + + ngx.say(#rows) + ngx.say(rows[1].key) + '; + } +--- request +GET /t +--- response_body +1 +local +--- no_error_log +[error] +--- error_log eval +qr/\[warn\].*?No cluster infos in shared dict/ + + + From 7ce4872c26db60ca7cf76a28363a3375e42784ea Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Wed, 9 Dec 2015 14:35:27 -0800 Subject: [PATCH 63/78] fix(tests) increase timeout for schema consensus test --- t/01-cassandra.t | 1 + 1 file changed, 1 insertion(+) diff --git a/t/01-cassandra.t b/t/01-cassandra.t index 46e9918..c9b26f8 100644 --- a/t/01-cassandra.t +++ b/t/01-cassandra.t @@ -179,6 +179,7 @@ local GET /t --- response_body +--- timeout: 5s --- no_error_log [error] From eced827b5486b00757f65a0695292a5e1799f7c8 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Wed, 9 Dec 2015 17:55:58 -0800 Subject: [PATCH 64/78] fix: boolean type inference + type inference tests --- spec/integration/cql_types_spec.lua | 59 +++++++++++++++++++++++++++++ src/cassandra/buffer/init.lua | 2 + 2 files changed, 61 insertions(+) diff --git a/spec/integration/cql_types_spec.lua b/spec/integration/cql_types_spec.lua index 9e4864f..5009eed 100644 --- a/spec/integration/cql_types_spec.lua +++ b/spec/integration/cql_types_spec.lua @@ -209,4 +209,63 @@ describe("CQL types integration", function() assert.equal(fixture.value[2], tuple[2]) end end) + + describe("type inference", function() + for _, fixture_type in ipairs({"ascii", "boolean", "float", "int", "text", "varchar"}) do + local fixture_values = utils.cql_fixtures[fixture_type] + it("["..fixture_type.."] should be inferred", function() + for _, fixture in ipairs(fixture_values) do + local insert_query = string.format("INSERT INTO all_types(id, %s_sample) VALUES(?, ?)", fixture_type) + local select_query = string.format("SELECT %s_sample FROM all_types WHERE id = ?", fixture_type) + + local res, err = session:execute(insert_query, {cassandra.uuid(_UUID), fixture}) + assert.falsy(err) + assert.truthy(res) + + local rows, err = session:execute(select_query, {cassandra.uuid(_UUID)}) + assert.falsy(err) + assert.truthy(rows) + + local decoded = rows[1][fixture_type.."_sample"] + assert.validFixture(fixture_type, fixture, decoded) + end + end) + end + + it("[map] should be inferred", function() + for _, fixture in ipairs(utils.cql_list_fixtures) do + local insert_query = string.format("INSERT INTO all_types(id, list_sample_%s) VALUES(?, ?)", fixture.type_name) + local select_query = string.format("SELECT list_sample_%s FROM all_types WHERE id = ?", fixture.type_name) + + local res, err = session:execute(insert_query, {cassandra.uuid(_UUID), fixture.value}) + assert.falsy(err) + assert.truthy(res) + + local rows, err = session:execute(select_query, {cassandra.uuid(_UUID)}) + assert.falsy(err) + assert.truthy(rows) + + local decoded = rows[1]["list_sample_"..fixture.type_name] + assert.validFixture("list", fixture.value, decoded) + end + end) + end) + + it("[set] should be inferred", function() + for _, fixture in ipairs(utils.cql_list_fixtures) do + local insert_query = string.format("INSERT INTO all_types(id, set_sample_%s) VALUES(?, ?)", fixture.type_name) + local select_query = string.format("SELECT set_sample_%s FROM all_types WHERE id = ?", fixture.type_name) + + local res, err = session:execute(insert_query, {cassandra.uuid(_UUID), fixture.value}) + assert.falsy(err) + assert.truthy(res) + + local rows, err = session:execute(select_query, {cassandra.uuid(_UUID)}) + assert.falsy(err) + assert.truthy(rows) + + local decoded = rows[1]["set_sample_"..fixture.type_name] + assert.sameSet(fixture.value, decoded) + end + end) end) diff --git a/src/cassandra/buffer/init.lua b/src/cassandra/buffer/init.lua index 792949b..2ca8def 100644 --- a/src/cassandra/buffer/init.lua +++ b/src/cassandra/buffer/init.lua @@ -126,6 +126,8 @@ function Buffer:repr_cql_value(value) else infered_type = cql_types.map end + elseif lua_type == "boolean" then + infered_type = cql_types.boolean else infered_type = cql_types.varchar end From 96c71419afc12c97bcdf589a840faf94486ca1d5 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Thu, 10 Dec 2015 19:53:24 -0800 Subject: [PATCH 65/78] feat(cluster) can now use sessions without cluster init --- README.md | 30 ++--- spec/integration/cassandra_spec.lua | 20 +++ spec/integration/error_handling_spec.lua | 10 ++ src/cassandra.lua | 119 ++++++++++-------- src/cassandra/cache.lua | 25 ++-- src/cassandra/log.lua | 4 +- t/01-cassandra.t | 12 +- t/02-no-cluster-init.t | 83 ++++++++++++ ...2-error-handling.t => 03-error-handling.t} | 10 +- 9 files changed, 217 insertions(+), 96 deletions(-) create mode 100644 t/02-no-cluster-init.t rename t/{02-error-handling.t => 03-error-handling.t} (92%) diff --git a/README.md b/README.md index 7d2518b..138660f 100644 --- a/README.md +++ b/README.md @@ -29,19 +29,6 @@ http { # all cluster informations will be stored here lua_shared_dict cassandra 1m; - init_by_lua ' - local cassandra = require "cassandra" - - -- retrieve cluster topology - local cluster, err = cassandra.spawn_cluster { - shm = "cassandra", -- defined by "lua_shared_dict" - contact_points = {"127.0.0.1", "127.0.0.2"} - } - if err then - ngx.log(ngx.ERR, "Could not spawn cluster: ", err.message) - end - '; - server { ... @@ -50,10 +37,11 @@ http { local cassandra = require "cassandra" local session, err = cassandra.spawn_session { - shm = "cassandra" -- defined by "lua_shared_dict" + shm = "cassandra", -- defined by "lua_shared_dict" + contact_points = {"127.0.0.1"} } if err then - ngx.log(ngx.ERR, "Could not spawn session: ", err.message) + ngx.log(ngx.ERR, "Could not spawn session: ", tostring(err)) return ngx.exit(500) end @@ -75,10 +63,11 @@ http { local cassandra = require "cassandra" local session, err = cassandra.spawn_session { - shm = "cassandra" -- defined by "lua_shared_dict" + shm = "cassandra", -- defined by "lua_shared_dict" + contact_points = {"127.0.0.1"} } if err then - ngx.log(ngx.ERR, "Could not spawn session: ", err.message) + ngx.log(ngx.ERR, "Could not spawn session: ", tostring(err)) return ngx.exit(500) end @@ -89,7 +78,7 @@ http { session:set_keep_alive() - ngx.say("number of users: ", #rows) + ngx.say("rows retrieved: ", #rows) '; } } @@ -101,15 +90,12 @@ With plain Lua: ```lua local cassandra = require "cassandra" -local cluster, err = cassandra.spawn_cluster { +local session, err = cassandra.spawn_session { shm = "cassandra", contact_points = {"127.0.0.1", "127.0.0.2"} } assert(err == nil) -local session, err = cluster:spawn_session() -assert(err == nil) - local res, err = session:execute("INSERT INTO users(id, name, age) VALUES(?, ?, ?)", { cassandra.uuid("1144bada-852c-11e3-89fb-e0b9a54a6d11"), "John O'Reilly", diff --git a/spec/integration/cassandra_spec.lua b/spec/integration/cassandra_spec.lua index b295ece..a13314e 100644 --- a/spec/integration/cassandra_spec.lua +++ b/spec/integration/cassandra_spec.lua @@ -96,6 +96,26 @@ describe("spawn_session()", function() assert.truthy(session.hosts) assert.equal(3, #session.hosts) end) + it("should spawn a session without having to spawn a cluster", function() + local shm = "session_without_cluster" + local session, err = cassandra.spawn_session { + shm = shm, + contact_points = _hosts + } + assert.falsy(err) + assert.truthy(session) + -- Check cache + local cache = require "cassandra.cache" + local hosts, err = cache.get_hosts(shm) + assert.falsy(err) + -- index of hosts + assert.equal(#_hosts, #hosts) + -- hosts details + for _, host_addr in ipairs(hosts) do + local host_details = cache.get_host(shm, host_addr) + assert.truthy(host_details) + end + end) describe(":execute()", function() teardown(function() -- drop keyspace in case tests failed diff --git a/spec/integration/error_handling_spec.lua b/spec/integration/error_handling_spec.lua index 1a8d55e..c637cc6 100644 --- a/spec/integration/error_handling_spec.lua +++ b/spec/integration/error_handling_spec.lua @@ -71,6 +71,16 @@ describe("error handling", function() assert.spy(options.parse_session).was.called() assert.equal("shm is required for spawning a cluster/session", err) end) + it("should error when spawning a session without contact_points not cluster", function() + local shm = "session_without_cluster_nor_contact_points" + local session, err = cassandra.spawn_session { + shm = shm + } + assert.truthy(err) + assert.falsy(session) + assert.equal("DriverError", err.type) + assert.equal("Options must contain contact_points to spawn session, or spawn a cluster in the init phase.", err.message) + end) end) describe("execute()", function() local session diff --git a/src/cassandra.lua b/src/cassandra.lua index 9ccce8a..9ba8d3c 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -704,12 +704,14 @@ function Session:new(options) if cache_err then return nil, cache_err elseif host_addresses == nil then - log.warn("No cluster infos in shared dict '"..session_options.shm.."'.") + log.warn("No cluster infos in shared dict "..session_options.shm) if session_options.contact_points ~= nil then host_addresses, err = Cassandra.refresh_hosts(session_options) if host_addresses == nil then return nil, err end + else + return nil, Errors.DriverError("Options must contain contact_points to spawn session, or spawn a cluster in the init phase.") end end @@ -893,74 +895,93 @@ local SELECT_LOCAL_QUERY = "SELECT data_center,rack,rpc_address,release_version --- Retrieve cluster informations from a connected contact_point function Cassandra.refresh_hosts(options) - log.info("Refreshing local and peers info") + local addresses = {} - local contact_points_hosts = {} - for _, contact_point in ipairs(options.contact_points) do - table_insert(contact_points_hosts, Host:new(contact_point, options)) + local lock, lock_err, elapsed = lock_mutex(options.shm, "refresh_hosts") + if lock_err then + return nil, lock_err end - local coordinator, err = RequestHandler.get_first_coordinator(contact_points_hosts) - if err then - return nil, err - end + if elapsed and elapsed == 0 then + log.info("Refreshing local and peers info") - local local_query = Requests.QueryRequest(SELECT_LOCAL_QUERY) - local peers_query = Requests.QueryRequest(SELECT_PEERS_QUERY) - local hosts = {} + local contact_points_hosts = {} + for _, contact_point in ipairs(options.contact_points) do + table_insert(contact_points_hosts, Host:new(contact_point, options)) + end - local rows, err = coordinator:send(local_query) - if err then - return nil, err - end - local row = rows[1] - local address = options.policies.address_resolution(row["rpc_address"]) - local local_host = { - datacenter = row["data_center"], - rack = row["rack"], - cassandra_version = row["release_version"], - protocol_versiom = row["native_protocol_version"], - unhealthy_at = 0, - reconnection_delay = 0 - } - hosts[address] = local_host - log.info("Local info retrieved") + local coordinator, err = RequestHandler.get_first_coordinator(contact_points_hosts) + if err then + return nil, err + end - rows, err = coordinator:send(peers_query) - if err then - return nil, err - end + local local_query = Requests.QueryRequest(SELECT_LOCAL_QUERY) + local peers_query = Requests.QueryRequest(SELECT_PEERS_QUERY) + local hosts = {} - for _, row in ipairs(rows) do - address = options.policies.address_resolution(row["rpc_address"]) - log.info("Adding host "..address) - hosts[address] = { + local rows, err = coordinator:send(local_query) + if err then + return nil, err + end + local row = rows[1] + local address = options.policies.address_resolution(row["rpc_address"]) + local local_host = { datacenter = row["data_center"], rack = row["rack"], cassandra_version = row["release_version"], - protocol_version = local_host.native_protocol_version, + protocol_versiom = row["native_protocol_version"], unhealthy_at = 0, reconnection_delay = 0 } - end - log.info("Peers info retrieved") - log.info(string_format("---- cluster spawned under shm %s ----", options.shm)) + hosts[address] = local_host + log.info("Local info retrieved") - coordinator:close() + rows, err = coordinator:send(peers_query) + if err then + return nil, err + end - -- Store cluster mapping for future sessions - local addresses = {} - for addr, host in pairs(hosts) do - table_insert(addresses, addr) - local ok, cache_err = cache.set_host(options.shm, addr, host) + for _, row in ipairs(rows) do + address = options.policies.address_resolution(row["rpc_address"]) + log.info("Adding host "..address) + hosts[address] = { + datacenter = row["data_center"], + rack = row["rack"], + cassandra_version = row["release_version"], + protocol_version = local_host.native_protocol_version, + unhealthy_at = 0, + reconnection_delay = 0 + } + end + log.info("Peers info retrieved") + log.info(string_format("Cluster infos retrieved in shared dict %s", options.shm)) + + coordinator:close() + + -- Store cluster mapping for future sessions + for addr, host in pairs(hosts) do + table_insert(addresses, addr) + local ok, cache_err = cache.set_host(options.shm, addr, host) + if not ok then + return nil, cache_err + end + end + + local ok, cache_err = cache.set_hosts(options.shm, addresses) if not ok then return nil, cache_err end + else + local cache_err + addresses, cache_err = cache.get_hosts(options.shm) + if cache_err then + return nil, cache_err + end end - local ok, err = cache.set_hosts(options.shm, addresses) - if not ok then - return nil, err + lock_err = unlock_mutex(lock) + if lock_err then + return nil, lock_err end return addresses diff --git a/src/cassandra/cache.lua b/src/cassandra/cache.lua index 67eaa6f..4569a9b 100644 --- a/src/cassandra/cache.lua +++ b/src/cassandra/cache.lua @@ -4,12 +4,7 @@ local Errors = require "cassandra.errors" local string_utils = require "cassandra.utils.string" local table_concat = table.concat local in_ngx = ngx ~= nil -local shared -if in_ngx then - shared = ngx.shared -else - shared = {} -end +local dicts = {} -- DICT Proxy -- https://github.com/bsm/fakengx/blob/master/fakengx.lua @@ -97,16 +92,20 @@ function SharedDict:flush_expired(n) end local function get_dict(shm) - if not in_ngx then - if shared[shm] == nil then - shared[shm] = SharedDict:new() - end - end + local dict = dicts[shm] - local dict = shared[shm] if dict == nil then - error("No shared dict named "..shm) + if in_ngx then + dict = ngx.shared[shm] + if dict == nil then + error("No shared dict named "..shm) + end + else + dict = SharedDict:new() + end + dicts[shm] = dict end + return dict end diff --git a/src/cassandra/log.lua b/src/cassandra/log.lua index e7e7796..21efd42 100644 --- a/src/cassandra/log.lua +++ b/src/cassandra/log.lua @@ -5,7 +5,9 @@ local is_ngx = ngx ~= nil local ngx_log = is_ngx and ngx.log +local ngx_get_phase = is_ngx and ngx.get_phase local string_format = string.format +local print = print -- ngx_lua levels redefinition for helpers and -- when outside of ngx_lua. @@ -31,7 +33,7 @@ end for lvl_name, lvl in pairs(LEVELS) do log[lvl_name:lower()] = function(...) - if is_ngx and ngx.get_phase() ~= "init" then + if is_ngx and ngx_get_phase() ~= "init" then ngx_log(ngx[lvl_name], ...) elseif lvl <= cur_lvl then print(string_format("%s -- %s", lvl_name, ...)) diff --git a/t/01-cassandra.t b/t/01-cassandra.t index c9b26f8..a4d7841 100644 --- a/t/01-cassandra.t +++ b/t/01-cassandra.t @@ -67,7 +67,7 @@ GET /t -=== TEST 2: session:execute() +=== TEST 3: session:execute() --- http_config eval "$::HttpConfig $::SpawnCluster" @@ -100,7 +100,7 @@ local -=== TEST 3: session:execute() with request arguments +=== TEST 4: session:execute() with request arguments --- http_config eval "$::HttpConfig $::SpawnCluster" @@ -133,7 +133,7 @@ local -=== TEST 4: wait for schema consensus +=== TEST 5: wait for schema consensus --- http_config eval "$::HttpConfig $::SpawnCluster" @@ -185,7 +185,7 @@ GET /t -=== TEST 5: session:shutdown() +=== TEST 6: session:shutdown() --- http_config eval "$::HttpConfig $::SpawnCluster" @@ -220,7 +220,7 @@ qr/\[error\].*?NoHostAvailableError: Cannot reuse a session that has been shut d -=== TEST 6: session:set_keep_alive() +=== TEST 7: session:set_keep_alive() --- http_config eval "$::HttpConfig $::SpawnCluster" @@ -255,7 +255,7 @@ GET /t -=== TEST 7: session:execute() prepared query +=== TEST 8: session:execute() prepared query --- http_config eval "$::HttpConfig $::SpawnCluster" diff --git a/t/02-no-cluster-init.t b/t/02-no-cluster-init.t new file mode 100644 index 0000000..d007bb2 --- /dev/null +++ b/t/02-no-cluster-init.t @@ -0,0 +1,83 @@ +use Test::Nginx::Socket::Lua; +use Cwd qw(cwd); + +repeat_each(1); + +plan tests => repeat_each() * blocks() * 4; + +my $pwd = cwd(); + +our $HttpConfig = <<_EOC_; + lua_package_path "$pwd/src/?.lua;$pwd/src/?/init.lua;;"; + lua_shared_dict cassandra 1m; +_EOC_ + +run_tests(); + +__DATA__ + +=== TEST 1: spawn session without cluster +--- http_config eval +"$::HttpConfig" +--- config + location /t { + content_by_lua ' + local cassandra = require "cassandra" + local session, err = cassandra.spawn_session { + shm = "cassandra", + contact_points = {"127.0.0.1"} + } + if err then + ngx.log(ngx.ERR, tostring(err)) + ngx.exit(500) + end + + local rows, err = session:execute("SELECT key FROM system.local") + if err then + ngx.log(ngx.ERR, tostring(err)) + ngx.exit(500) + else + ngx.say("type: "..rows.type) + ngx.say("#rows: "..#rows) + for _, row in ipairs(rows) do + ngx.say(row["key"]) + end + end + '; + } +--- request +GET /t +--- response_body +type: ROWS +#rows: 1 +local +--- no_error_log +[error] +--- error_log eval +[ + qr/\[warn\].*?No cluster infos in shared dict/, + qr/\[info\].*?Cluster infos retrieved in shared dict cassandra/ +] + + + +=== TEST 2: spawn session without cluster nor contact_points option +--- http_config eval +"$::HttpConfig" +--- config + location /t { + content_by_lua ' + local cassandra = require "cassandra" + local session, err = cassandra.spawn_session { + shm = "cassandra" + } + if err then + ngx.log(ngx.ERR, tostring(err)) + end + '; + } +--- request +GET /t +--- response_body + +--- error_log: Options must contain contact_points to spawn session, or spawn a cluster in the init phase. diff --git a/t/02-error-handling.t b/t/03-error-handling.t similarity index 92% rename from t/02-error-handling.t rename to t/03-error-handling.t index 69ce63e..d6166db 100644 --- a/t/02-error-handling.t +++ b/t/03-error-handling.t @@ -3,7 +3,7 @@ use Cwd qw(cwd); repeat_each(2); -plan tests => repeat_each() * blocks() * 4; +plan tests => repeat_each() * blocks() * 5; my $pwd = cwd(); @@ -92,7 +92,7 @@ local --- no_error_log [error] --- error_log eval -qr/\[warn\].*?No cluster infos in shared dict/ - - - +[ + qr/\[warn\].*?No cluster infos in shared dict/, + qr/\[info\].*?Cluster infos retrieved in shared dict cassandra/ +] From da7445e7ae6dab790ff303bd830dad26eb6ebcf3 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Fri, 11 Dec 2015 00:01:45 -0800 Subject: [PATCH 66/78] feat: drop the lua-cjson depenency We simply serialize less values in the shm, as the library currently only uses unhealthy_at and reconnection_delay. --- lua-cassandra-0.4.0-0.rockspec | 3 +-- src/cassandra.lua | 16 ++++++++-------- src/cassandra/cache.lua | 19 +++++++++++++++---- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/lua-cassandra-0.4.0-0.rockspec b/lua-cassandra-0.4.0-0.rockspec index 18468df..2cc3182 100644 --- a/lua-cassandra-0.4.0-0.rockspec +++ b/lua-cassandra-0.4.0-0.rockspec @@ -10,8 +10,7 @@ description = { license = "MIT" } dependencies = { - "luasocket ~> 3.0-rc1", - "lua-cjson ~> 2.1.0-1" + "luasocket ~> 3.0-rc1" } build = { type = "builtin", diff --git a/src/cassandra.lua b/src/cassandra.lua index 9ba8d3c..dab274b 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -926,10 +926,10 @@ function Cassandra.refresh_hosts(options) local row = rows[1] local address = options.policies.address_resolution(row["rpc_address"]) local local_host = { - datacenter = row["data_center"], - rack = row["rack"], - cassandra_version = row["release_version"], - protocol_versiom = row["native_protocol_version"], + --datacenter = row["data_center"], + --rack = row["rack"], + --cassandra_version = row["release_version"], + --protocol_versiom = row["native_protocol_version"], unhealthy_at = 0, reconnection_delay = 0 } @@ -945,10 +945,10 @@ function Cassandra.refresh_hosts(options) address = options.policies.address_resolution(row["rpc_address"]) log.info("Adding host "..address) hosts[address] = { - datacenter = row["data_center"], - rack = row["rack"], - cassandra_version = row["release_version"], - protocol_version = local_host.native_protocol_version, + --datacenter = row["data_center"], + --rack = row["rack"], + --cassandra_version = row["release_version"], + --protocol_version = local_host.native_protocol_version, unhealthy_at = 0, reconnection_delay = 0 } diff --git a/src/cassandra/cache.lua b/src/cassandra/cache.lua index 4569a9b..841ee21 100644 --- a/src/cassandra/cache.lua +++ b/src/cassandra/cache.lua @@ -1,13 +1,19 @@ local log = require "cassandra.log" -local json = require "cjson" local Errors = require "cassandra.errors" local string_utils = require "cassandra.utils.string" + local table_concat = table.concat +local tonumber = tonumber + local in_ngx = ngx ~= nil local dicts = {} --- DICT Proxy +-- ngx.shared.DICT proxy -- https://github.com/bsm/fakengx/blob/master/fakengx.lua +-- Used when the driver is required outside of ngx_lua. +-- Eventually, attaching the cluster infos to the session +-- should be done standardly, but this is a faster alternative +-- for now. local SharedDict = {} @@ -139,7 +145,7 @@ end local function set_host(shm, host_addr, host) local dict = get_dict(shm) - local ok, err = dict:safe_set(host_addr, json.encode(host)) + local ok, err = dict:safe_set(host_addr, host.unhealthy_at.._SEP..host.reconnection_delay) if not ok then return false, Errors.SharedDictError("Cannot store host details for cluster "..shm..": "..err, shm) end @@ -154,7 +160,12 @@ local function get_host(shm, host_addr) elseif value == nil then return nil, Errors.DriverError("No details for host "..host_addr.." under shm "..shm) end - return json.decode(value) + + local h = string_utils.split(value, _SEP) + return { + unhealthy_at = tonumber(h[1]), + reconnection_delay = tonumber(h[2]) + } end --- Prepared query ids From 9bc9c01920ded6307d9c8a50481bf8e240896b2f Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Fri, 11 Dec 2015 00:02:43 -0800 Subject: [PATCH 67/78] docs(readme) better example, test and tools doc --- Makefile | 2 +- README.md | 105 ++++++++++++++++++++++++--------- lua-cassandra-0.4.0-0.rockspec | 2 +- 3 files changed, 79 insertions(+), 30 deletions(-) diff --git a/Makefile b/Makefile index d829544..278a0b8 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ DEV_ROCKS=busted luacov luacov-coveralls luacheck ldoc -.PHONY: instal dev clean test coverage lint doc +.PHONY: install dev clean test coverage lint doc install: @luarocks make lua-cassandra-*.rockspec diff --git a/README.md b/README.md index 138660f..924f472 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,16 @@ A pure Lua client library for Apache Cassandra (2.0+), compatible with Lua and [ngx_lua]. -It is build on the model of the official Datastax drivers, and tries to implement the same behaviors and features. +It is build following the example of the official Datastax drivers, and tries to implement the same behaviors, options and features. + +## Table of Contents + +- [Features](#features) +- [Usage](#usage) +- [Installation](#installation) +- [Documentation and Examples](#documentation-and-examples) +- [Test Suite](#test-suite) +- [Tools](#tools) ## Features @@ -13,7 +22,7 @@ It is build on the model of the official Datastax drivers, and tries to implemen - Configurable load balancing, reconnection and retry policies - TLS client-to-node encryption - Client authentication -- Highly configurable options per session/request +- Highly configurable options per session/query - Compatible with Cassandra 2.0 and 2.1 - Works with Lua 5.1, 5.2, 5.3 and LuaJIT 2.x @@ -32,7 +41,7 @@ http { server { ... - location /insert { + location / { content_by_lua ' local cassandra = require "cassandra" @@ -54,23 +63,6 @@ http { -- ... end - session:set_keep_alive() - '; - } - - location /get { - content_by_lua ' - local cassandra = require "cassandra" - - local session, err = cassandra.spawn_session { - shm = "cassandra", -- defined by "lua_shared_dict" - contact_points = {"127.0.0.1"} - } - if err then - ngx.log(ngx.ERR, "Could not spawn session: ", tostring(err)) - return ngx.exit(500) - end - local rows, err = session:execute("SELECT * FROM users") if err then -- ... @@ -106,7 +98,7 @@ assert(err == nil) local rows, err = session:execute("SELECT * FROM users") assert(err == nil) -print("number of users: ", #rows) +print("rows retrieved: ", #rows) session:shutdown() ``` @@ -119,26 +111,83 @@ With [Luarocks]: $ luarocks install lua-cassandra ``` -If installed manually, this module requires: +Manually: -- [lua-cjson](https://github.com/mpx/lua-cjson/) -- [LuaSocket](http://w3.impa.br/~diego/software/luasocket/) -- If you wish to use TLS client-to-node encryption, [LuaSec](https://github.com/brunoos/luasec) - -Once you have a local copy of this module's files under `src/`, add this to your Lua package path: +Once you have a local copy of this module's `src/` fo lder, add it to your `LUA_PATH` (or `lua_package_path` for ngx_lua): ``` /path/to/src/?.lua;/path/to/src/?/init.lua; ``` +**Note**: If used *outside* of ngx_lua, this module requires: + +- [LuaSocket](http://w3.impa.br/~diego/software/luasocket/) +- If you wish to use TLS client-to-node encryption, [LuaSec](https://github.com/brunoos/luasec) + ## Documentation and examples The current [documentation] targets version `0.3.6` only. `0.4.0` documentation should come soon. +## Test Suite + +This library uses three test suites: + +- Unit tests, with busted +- Integration tests, with busted and a running Cassandra cluster +- ngx_lua integration tests with Test::Nginx::Socket and a running Cassandra cluster + +- The first can simply be run after installing [busted](http://olivinelabs.com/busted/) and running: + +```shell +$ busted spec/unit +``` + +- The integration tests are located in another folder, and require a Cassandra instance (currently 2.1+) to be running. Your cluster's hosts (not just the contact points, but all of them) should be declared in the `HOSTS` environment variable: + +```shell +$ HOSTS=127.0.0.1,127.0.0.2,127.0.0.3 busted spec/integration +``` + +Finally, the ngx_lua integration tests can be run after installing the [Test::Nginx::Socket](http://search.cpan.org/~agent/Test-Nginx-0.23/lib/Test/Nginx/Socket.pm) module and also require a Cassandra instance to run on `localhost`: + +```shell +$ prove t/ +``` + +## Tools + +This module can also use various tools for code quality, they can easily be installed from Luarocks by running: + +``` +$ make dev +``` + +Code coverage is analyzed by [luacov](http://keplerproject.github.io/luacov/) from the **busted** (unit and integration) tests: + +```shell +$ busted --coverage +$ luacov cassandra +# or +$ make coverage +``` + +The code is linted with [luacheck](https://github.com/mpeterv/luacheck). It is easier to use the Makefile again to avoid analyzing Lua files that are not part of this module: + +```shell +$ make lint +``` + +The documentation is generated by [ldoc](https://github.com/stevedonovan/LDoc) and can be generated with: + +```shell +$ ldoc -c doc/config.ld src +# or +$ make doc +``` + [ngx_lua]: https://github.com/openresty/lua-nginx-module [Luarocks]: https://luarocks.org -[lua-resty-cassandra]: https://github.com/jbochi/lua-resty-cassandra [documentation]: http://thibaultcha.github.io/lua-cassandra/ [manual]: http://thibaultcha.github.io/lua-cassandra/manual/README.md.html diff --git a/lua-cassandra-0.4.0-0.rockspec b/lua-cassandra-0.4.0-0.rockspec index 2cc3182..cff199f 100644 --- a/lua-cassandra-0.4.0-0.rockspec +++ b/lua-cassandra-0.4.0-0.rockspec @@ -5,7 +5,7 @@ source = { tag = "0.4.0" } description = { - summary = "Lua Cassandra client library", + summary = "Feature-rich client library for Cassandra", homepage = "http://thibaultcha.github.io/lua-cassandra", license = "MIT" } From 2d8207b09d2ab2d232ce3c0effae68c69ab3ceb9 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Fri, 11 Dec 2015 01:54:48 -0800 Subject: [PATCH 68/78] docs(ldoc) start documenting the main API --- doc/examples/authentication.lua.html | 13 +- doc/examples/basic.lua.html | 11 +- doc/examples/batch.lua.html | 11 +- doc/examples/pagination.lua.html | 11 +- doc/examples/ssl.lua.html | 11 +- doc/index.html | 423 +++++++++++++++++++++++---- doc/manual/README.md.html | 15 +- doc/modules/Cassandra.html | 102 +++---- spec/integration/cassandra_spec.lua | 16 +- spec/integration/cql_types_spec.lua | 7 +- src/cassandra.lua | 169 +++++++++-- src/cassandra/log.lua | 2 +- 12 files changed, 586 insertions(+), 205 deletions(-) diff --git a/doc/examples/authentication.lua.html b/doc/examples/authentication.lua.html index 01f2e3f..855d1ef 100644 --- a/doc/examples/authentication.lua.html +++ b/doc/examples/authentication.lua.html @@ -26,9 +26,6 @@

lua-cassandra

- @@ -42,11 +39,7 @@

Examples

Modules

Manual

    @@ -60,7 +53,7 @@

    Manual

    authentication.lua

     --------
    --- Example with the PasswordAuthenticator IAuthenticator
    +-- Example with the PasswordAuthenticator IAuthenticator
     -- @see http://docs.datastax.com/en/cassandra/1.2/cassandra/security/security_config_native_authenticate_t.html
     
     local cassandra = require "cassandra"
    @@ -79,7 +72,7 @@ 

    authentication.lua

    generated by LDoc 1.4.3 -Last updated 2015-08-14 20:05:42 +Last updated 2015-12-11 01:52:31
    diff --git a/doc/examples/basic.lua.html b/doc/examples/basic.lua.html index 8628e5f..64f1288 100644 --- a/doc/examples/basic.lua.html +++ b/doc/examples/basic.lua.html @@ -26,9 +26,6 @@

    lua-cassandra

    - @@ -42,11 +39,7 @@

    Examples

Modules

Manual

    @@ -101,7 +94,7 @@

    basic.lua

    generated by LDoc 1.4.3 -Last updated 2015-08-14 20:05:42 +Last updated 2015-12-11 01:52:31
    diff --git a/doc/examples/batch.lua.html b/doc/examples/batch.lua.html index b7aed5b..02072d5 100644 --- a/doc/examples/batch.lua.html +++ b/doc/examples/batch.lua.html @@ -26,9 +26,6 @@

    lua-cassandra

    - @@ -42,11 +39,7 @@

    Examples

Modules

Manual

    @@ -81,7 +74,7 @@

    batch.lua

    generated by LDoc 1.4.3 -Last updated 2015-08-14 20:05:42 +Last updated 2015-12-11 01:52:31
    diff --git a/doc/examples/pagination.lua.html b/doc/examples/pagination.lua.html index 810ecbb..65cc58a 100644 --- a/doc/examples/pagination.lua.html +++ b/doc/examples/pagination.lua.html @@ -26,9 +26,6 @@

    lua-cassandra

    - @@ -42,11 +39,7 @@

    Examples

Modules

Manual

    @@ -91,7 +84,7 @@

    pagination.lua

    generated by LDoc 1.4.3 -Last updated 2015-08-14 20:05:42 +Last updated 2015-12-11 01:52:31
    diff --git a/doc/examples/ssl.lua.html b/doc/examples/ssl.lua.html index b26f370..369c62e 100644 --- a/doc/examples/ssl.lua.html +++ b/doc/examples/ssl.lua.html @@ -26,9 +26,6 @@

    lua-cassandra

    - @@ -42,11 +39,7 @@

    Examples

Modules

Manual

    @@ -121,7 +114,7 @@

    ssl.lua

    generated by LDoc 1.4.3 -Last updated 2015-08-14 20:05:42 +Last updated 2015-12-11 01:52:31
    diff --git a/doc/index.html b/doc/index.html index 1a68c5b..4a9b6bd 100644 --- a/doc/index.html +++ b/doc/index.html @@ -27,15 +27,16 @@

    lua-cassandra

    +

    Contents

    +

    Modules

    Manual

      @@ -54,69 +55,389 @@

      Examples

      +

      Module cassandra

      +

      Cassandra client library for Lua.

      +

      -

      A Lua Cassandra client for binary protocol v2 and v3

      +

      -

      Modules

      - - - - - - - - - - - - - - - - - - - - - -
      CassandraThis module allows the creation of sessions and provides shorthand - annotations for type encoding and batch statement creation.
      PasswordAuthenticatorThe client authenticator for the Cassandra PasswordAuthenticator IAuthenticator.
      BatchStatementThis module represents a Cassandra batch statement.
      ErrorEvery error is represented by a table with the following properties.
      SessionThis module provides a session to interact with a Cassandra cluster.
      -

      Manual

      - + +

      Session

      +
      - - + +
      README.mdSession:execute (query, args, query_options)Execute a query.
      -

      Examples

      - - - - - +

      Cassandra

      +
      authentication.lua
      - - + + - - + + - - - - - - + +
      basic.luaCassandra.spawn_session (options)Spawn a session to connect to the cluster.
      batch.luaCassandra.spawn_cluster (options)Load the cluster topology.
      pagination.lua
      ssl.luatype_serializersType serializer shorthands.
      +
      +
      + + +

      Session

      + +
      +
      + + Session:execute (query, args, query_options) +
      +
      + +

      Execute a query. + The session will choose a coordinator from the cached cluster topology according + to the configured load balancing policy. Once connected to it, it will perform the + given query. This operation is non-blocking in the context of ngx_lua because it + uses the cosocket API (coroutine based).

      + +

      Queries can have parameters binded to it, they can be prepared, result sets can + be paginated, and more, depending on the given query_options.

      + +

      If a node is not responding, it will be marked as being down. Since the state of + the cluster is shared accross all workers by the ngx.shared.DICT API, all the other + active sessions will be aware of it and will not attempt to connect to that node. The + configured reconnection policy will decide when it is time to try to connect to that + node again.

      + +
       local res, err = session:execute [[
      +   CREATE KEYSPACE IF NOT EXISTS my_keyspace
      +   WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor': 1}
      + ]]
      +
      + local rows, err = session:execute("SELECT * FROM system.schema_keyspaces")
      + for i, row in ipairs(rows) do
      +   print(row.keyspace_name)
      + end
      +
      + local rows, err = session:execute("SELECT * FROM users WHERE age = ?", {42}, {prepare = true})
      + for i, row in ipairs(rows) do
      +   print(row.username)
      + end
      +
      + + + + +

      Parameters:

      +
        +
      • query + string + The CQL query to execute, possibly with placeholder for binded parameters. +
      • +
      • args + table + Optional A list of parameters to be binded to the query's placeholders. +
      • +
      • query_options + table + Optional Override the sessions options.query_options` with the given values, for this request only. +
      • +
      + +

      Returns:

      +
        +
      1. + table + result/rows: A table describing the result. The content of this table depends on the type of the query. If an error occurred, this value will be nil and a second value is returned describing the error.
      2. +
      3. + table + error: A table describing the error that occurred.
      4. +
      + + + + +
      +
      +

      Cassandra

      + +
      +
      + + Cassandra.spawn_session (options) +
      +
      + +

      Spawn a session to connect to the cluster. + Sessions are meant to be short-lived and many can be created in parallel. In the context + of ngx_lua, it makes perfect sense for a session to be spawned in a phase handler, and + quickly disposed of by putting the sockets it used back into the cosocket connection pool.

      + +

      The session will retrieve the cluster topology from the configured shared dict or, + if not found, by connecting to one of the optionally given contact_points. + If you want to pre-load the cluster topology, see spawn_cluster. + The created session will use the configured load balancing policy to choose a + coordinator from the retrieved cluster topology on each query.

      + +
       access_by_lua_block {
      +     local cassandra = require "cassandra"
      +     local session, err = cassandra.spawn_session {
      +         shm = "cassandra",
      +         contact_points = {"127.0.0.1", "127.0.0.2"}
      +     }
      +     if not session then
      +         ngx.log(ngx.ERR, tostring(err))
      +     end
      +
      +     -- execute query(ies)
      +
      +     session:set_keep_alive()
      + }
      +
      + + + + +

      Parameters:

      +
        +
      • options + table + The session's options, including the shared dict name and *optionally the contact points +
      • +
      + +

      Returns:

      +
        +
      1. + table + session: A instanciated session, ready to be used. If an error occurred, this value will be nil and a second value is returned describing the error.
      2. +
      3. + table + error: A table describing the error that occurred.
      4. +
      + + + + +
      +
      + + Cassandra.spawn_cluster (options) +
      +
      + +

      Load the cluster topology. + Iterate over the given contact_points and connects to the first one available + to load the cluster topology. All peers of the chosen contact point will be + retrieved and stored locally so that the load balancing policy can chose one + on each request that will be executed.

      + +

      Use this function if you want to retrieve the cluster topology sooner than when + you will create your first Session. For example:

      + +
       init_worker_by_lua_block {
      +     local cassandra = require "cassandra"
      +     local cluster, err = cassandra.spawn_cluster {
      +          shm = "cassandra",
      +          contact_points = {"127.0.0.1"}
      +     }
      + }
      +
      + access_by_lua_block {
      +     local cassandra = require "cassandra"
      +     -- The cluster topology is already loaded at this point,
      +     -- avoiding latency on your first request.
      +     local session, err = cassandra.spawn_session {
      +         shm = "cassandra"
      +     }
      + }
      +
      + + + + +

      Parameters:

      +
        +
      • options + table + The cluster's options, including the shared dict name and the contact points. +
      • +
      + +

      Returns:

      +
        +
      1. + boolean + ok: Success of the cluster topology retrieval. If false, a second value will be returned describing the error.
      2. +
      3. + table + error: An error in case the operation did not succeed.
      4. +
      + + + + +
      +
      + + type_serializers +
      +
      + Type serializer shorthands. + When binding parameters to a query from execute, some + types cannot be infered automatically and will require manual + serialization. Some other times, it can be useful to manually enforce + the type of a parameter.

      + +

      See the Cassandra Data Types.

      + +

      For this purpose, shorthands for type serialization are available + on the Cassandra table: + + +

      Fields:

      +
        +
      • uuid + +

        Serialize a 32 lowercase characters string to a uuid

        +
         cassandra.uuid("123e4567-e89b-12d3-a456-426655440000")
        +
        + +
      • +
      • timestamp + +

        Serialize a 10 digits number into a Cassandra timestamp

        +
         cassandra.timestamp(1405356926)
        +
        + +
      • +
      • list + + +
         cassandra.list({"abc", "def"})
        +
        + +
      • +
      • map + + +
         cassandra.map({foo = "bar"})
        +
        + +
      • +
      • set + + +
         cassandra.set({foo = "bar"})
        +
        + +
      • +
      • udt + + + +
      • +
      • tuple + + + +
      • +
      • inet + + +
         cassandra.inet("127.0.0.1")
        + cassandra.inet("2001:0db8:85a3:0042:1000:8a2e:0370:7334")
        +
        + +
      • +
      • bigint + + +
         cassandra.bigint(42000000000)
        +
        + +
      • +
      • double + + +
         cassandra.bigint(1.0000000000000004)
        +
        + +
      • +
      • ascii + + + +
      • +
      • blob + + + +
      • +
      • boolean + + + +
      • +
      • counter + + + +
      • +
      • decimal + + + +
      • +
      • float + + + +
      • +
      • int + + + +
      • +
      • text + + + +
      • +
      • timeuuid + + + +
      • +
      • varchar + + + +
      • +
      • varint + + + +
      • +
      + + + + + +
      +
      + +
      generated by LDoc 1.4.3 -Last updated 2015-08-14 20:05:42 +Last updated 2015-12-11 01:52:31
      diff --git a/doc/manual/README.md.html b/doc/manual/README.md.html index 404c2cd..3a41981 100644 --- a/doc/manual/README.md.html +++ b/doc/manual/README.md.html @@ -26,9 +26,6 @@

      lua-cassandra

      -

      Contents

        @@ -43,11 +40,7 @@

        Manual

      Modules

      Examples

        @@ -125,9 +118,9 @@

        Usage

        -

        See the cassandra module for a detailed list of available functions.

        +

        See the cassandra module for a detailed list of available functions.

        -

        Once you have an instance of cassandra, use it to create sessions. See the session module for a detailed list of functions.

        +

        Once you have an instance of cassandra, use it to create sessions. See the session module for a detailed list of functions.

        Finally, check the examples section for concrete examples of basic or advanced usage.

        @@ -137,7 +130,7 @@

        Usage

        generated by LDoc 1.4.3 -Last updated 2015-08-14 20:05:42 +Last updated 2015-12-11 01:52:31
        diff --git a/doc/modules/Cassandra.html b/doc/modules/Cassandra.html index 90bd28e..d2371d7 100644 --- a/doc/modules/Cassandra.html +++ b/doc/modules/Cassandra.html @@ -32,17 +32,14 @@

        lua-cassandra

        Contents

        Modules

        Manual

          @@ -61,42 +58,22 @@

          Examples

          -

          Module Cassandra

          -

          This module allows the creation of sessions and provides shorthand - annotations for type encoding and batch statement creation.

          +

          Module cassandra

          +

          Constants

          -

          Depending on how it will be initialized, it supports either the binary - protocol v2 or v3:

          - - -
          -require "cassandra" -- binary protocol v3 (Cassandra 2.0.x and 2.1.x)
          -require "cassandra.v2" -- binary procotol v2 (Cassandra 2.0.x)
          -
          -
          - -

          Shorthands to give a type to a value in a query:

          - - -
          -session:execute("SELECT * FROM users WHERE id = ?", {
          -  cassandra.uuid("2644bada-852c-11e3-89fb-e0b9a54a6d93")
          -})
          -
          -

          -

          Functions

          +

          Cassandra

          - - + + - - + +
          new ()Instanciate a new Session.Cassandra.refresh_hosts (options)Retrieve cluster informations from a connected contact_point
          BatchStatement (batch_type)Instanciate a BatchStatement.Cassandra.spawn_cluster (options)Load the cluster topology.
          @@ -104,49 +81,60 @@

          Functions


          -

          Functions

          +

          Cassandra

          - - new () + + Cassandra.refresh_hosts (options)
          - Instanciate a new Session. - Create a socket with the cosocket API if in Nginx and available, fallback to luasocket otherwise. - The instanciated session will communicate using the binary protocol of the current cassandra - implementation being required. + Retrieve cluster informations from a connected contact_point +

          Parameters:

          +
            +
          • options + + + +
          • +
          -

          Returns:

          -
            -
          1. - session The created session.
          2. -
          3. - err Any Error encountered during the socket creation.
          4. -
          - - BatchStatement (batch_type) + + Cassandra.spawn_cluster (options)
          - Instanciate a BatchStatement. - The instanciated batch will then provide an ":add()" method to add queries, - and can be executed by a session's ":execute()" function. - See the related BatchStatement module and batch.lua example. - See http://docs.datastax.com/en/cql/3.1/cql/cqlreference/batchr.html + +

          Load the cluster topology. + Iterates over the given contact_points and connects to the first one available + to load the cluster topology. All peers of the chosen contact point will be + retrieved and stored locally so that the load balancing policy can chose one + on each request that will be executed. + Use this function if you want to retrieve the cluster topology sooner than when + you will create your first Session. For example:

          + +
           local hello = "world"
          +
          + + + + +

          Parameters:

            -
          • batch_type - The type of this batch. Can be one of: 'Logged, Unlogged, Counter' +
          • options + + +
          @@ -162,7 +150,7 @@

          Parameters:

          generated by LDoc 1.4.3 -Last updated 2015-08-14 20:05:42 +Last updated 2015-12-11 00:21:07
          diff --git a/spec/integration/cassandra_spec.lua b/spec/integration/cassandra_spec.lua index a13314e..f766ac5 100644 --- a/spec/integration/cassandra_spec.lua +++ b/spec/integration/cassandra_spec.lua @@ -16,12 +16,12 @@ local _hosts = utils.hosts describe("spawn_cluster()", function() it("should spawn a cluster", function() - local cluster, err = cassandra.spawn_cluster { + local ok, err = cassandra.spawn_cluster { shm = _shm, contact_points = _hosts } assert.falsy(err) - assert.truthy(cluster) + assert.True(ok) end) it("should retrieve cluster infos in spawned cluster's shm", function() local cache = require "cassandra.cache" @@ -44,12 +44,12 @@ describe("spawn_cluster()", function() local contact_points = {"0.0.0.1", "0.0.0.2", "0.0.0.3"} contact_points[#contact_points + 1] = _hosts[1] - local cluster, err = cassandra.spawn_cluster({ + local ok, err = cassandra.spawn_cluster({ shm = "test", contact_points = contact_points }) assert.falsy(err) - assert.truthy(cluster) + assert.True(ok) end) it("should accept a custom port for given hosts", function() utils.set_log_lvl("QUIET") @@ -61,12 +61,12 @@ describe("spawn_cluster()", function() for i, addr in ipairs(_hosts) do contact_points[i] = addr..":9043" end - local cluster, err = cassandra.spawn_cluster({ + local ok, err = cassandra.spawn_cluster({ shm = "test", contact_points = contact_points }) assert.truthy(err) - assert.falsy(cluster) + assert.False(ok) assert.equal("NoHostAvailableError", err.type) end) it("should accept a custom port through an option", function() @@ -75,13 +75,13 @@ describe("spawn_cluster()", function() utils.set_log_lvl(LOG_LVL) end) - local cluster, err = cassandra.spawn_cluster({ + local ok, err = cassandra.spawn_cluster({ shm = "test", protocol_options = {default_port = 9043}, contact_points = _hosts }) assert.truthy(err) - assert.falsy(cluster) + assert.False(ok) assert.equal("NoHostAvailableError", err.type) end) end) diff --git a/spec/integration/cql_types_spec.lua b/spec/integration/cql_types_spec.lua index 5009eed..eaa69a9 100644 --- a/spec/integration/cql_types_spec.lua +++ b/spec/integration/cql_types_spec.lua @@ -12,13 +12,10 @@ describe("CQL types integration", function() local session setup(function() - local cluster, err = cassandra.spawn_cluster({ + local session, err = cluster:spawn_session { shm = _shm, contact_points = _hosts - }) - assert.falsy(err) - - session, err = cluster:spawn_session({shm = _shm}) + } assert.falsy(err) utils.create_keyspace(session, _keyspace) diff --git a/src/cassandra.lua b/src/cassandra.lua index dab274b..dc8cfb2 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -1,10 +1,9 @@ +--- Cassandra client library for Lua. +-- @module cassandra + -- @TODO --- flush from dict on shutdown +-- flush dicts on shutdown? -- tracing --- --- better logging --- more options validation --- more error types local log = require "cassandra.log" local opts = require "cassandra.options" @@ -52,13 +51,11 @@ local function unlock_mutex(lock) end --- Constants --- @section constants local MIN_PROTOCOL_VERSION = 2 local DEFAULT_PROTOCOL_VERSION = 3 --- Cassandra --- @section cassandra local Cassandra = { _VERSION = "0.4.0", @@ -70,7 +67,6 @@ local Cassandra = { --- Host -- A connection to a single host. -- Not cluster aware, only maintain a socket to its peer. --- @section host local Host = {} Host.__index = Host @@ -459,7 +455,6 @@ function Host:can_be_considered_up() end --- Request Handler --- @section request_handler local RequestHandler = {} RequestHandler.__index = RequestHandler @@ -684,7 +679,6 @@ function RequestHandler:prepare_and_retry(request) end --- Session --- A short-lived session, cluster-aware through the cache. -- @section session local Session = {} @@ -813,6 +807,41 @@ local function page_iterator(request_handler, query, args, query_options) end, query, nil end +--- Execute a query. +-- The session will choose a coordinator from the cached cluster topology according +-- to the configured load balancing policy. Once connected to it, it will perform the +-- given query. This operation is non-blocking in the context of ngx_lua because it +-- uses the cosocket API (coroutine based). +-- +-- Queries can have parameters binded to it, they can be prepared, result sets can +-- be paginated, and more, depending on the given `query_options`. +-- +-- If a node is not responding, it will be marked as being down. Since the state of +-- the cluster is shared accross all workers by the ngx.shared.DICT API, all the other +-- active sessions will be aware of it and will not attempt to connect to that node. The +-- configured reconnection policy will decide when it is time to try to connect to that +-- node again. +-- +-- local res, err = session:execute [[ +-- CREATE KEYSPACE IF NOT EXISTS my_keyspace +-- WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor': 1} +-- ]] +-- +-- local rows, err = session:execute("SELECT * FROM system.schema_keyspaces") +-- for i, row in ipairs(rows) do +-- print(row.keyspace_name) +-- end +-- +-- local rows, err = session:execute("SELECT * FROM users WHERE age = ?", {42}, {prepare = true}) +-- for i, row in ipairs(rows) do +-- print(row.username) +-- end +-- +-- @param[type=string] query The CQL query to execute, possibly with placeholder for binded parameters. +-- @param[type=table] args *Optional* A list of parameters to be binded to the query's placeholders. +-- @param[type=table] query_options *Optional* Override the session`s `options.query_options` with the given values, for this request only. +-- @treturn table `result/rows`: A table describing the result. The content of this table depends on the type of the query. If an error occurred, this value will be `nil` and a second value is returned describing the error. +-- @treturn table `error`: A table describing the error that occurred. function Session:execute(query, args, query_options) if self.terminated then return nil, Errors.NoHostAvailableError("Cannot reuse a session that has been shut down.") @@ -886,6 +915,35 @@ end --- Cassandra -- @section cassandra +--- Spawn a session to connect to the cluster. +-- Sessions are meant to be short-lived and many can be created in parallel. In the context +-- of ngx_lua, it makes perfect sense for a session to be spawned in a phase handler, and +-- quickly disposed of by putting the sockets it used back into the cosocket connection pool. +-- +-- The session will retrieve the cluster topology from the configured shared dict or, +-- if not found, by connecting to one of the optionally given `contact_points`. +-- If you want to pre-load the cluster topology, see `spawn_cluster`. +-- The created session will use the configured load balancing policy to choose a +-- coordinator from the retrieved cluster topology on each query. +-- +-- access_by_lua_block { +-- local cassandra = require "cassandra" +-- local session, err = cassandra.spawn_session { +-- shm = "cassandra", +-- contact_points = {"127.0.0.1", "127.0.0.2"} +-- } +-- if not session then +-- ngx.log(ngx.ERR, tostring(err)) +-- end +-- +-- -- execute query(ies) +-- +-- session:set_keep_alive() +-- } +-- +-- @param[type=table] options The session's options, including the shared dict name and **optionally* the contact points +-- @treturn table `session`: A instanciated session, ready to be used. If an error occurred, this value will be `nil` and a second value is returned describing the error. +-- @treturn table `error`: A table describing the error that occurred. function Cassandra.spawn_session(options) return Session:new(options) end @@ -893,7 +951,7 @@ end local SELECT_PEERS_QUERY = "SELECT peer,data_center,rack,rpc_address,release_version FROM system.peers" local SELECT_LOCAL_QUERY = "SELECT data_center,rack,rpc_address,release_version FROM system.local WHERE key='local'" ---- Retrieve cluster informations from a connected contact_point +-- Retrieve cluster informations from a connected contact_point function Cassandra.refresh_hosts(options) local addresses = {} @@ -987,34 +1045,93 @@ function Cassandra.refresh_hosts(options) return addresses end -local Cluster = {} -Cluster.__index = Cluster - -function Cluster:spawn_session(options) - options = table_utils.extend_table(self.options, options) - return Cassandra.spawn_session(options) -end - ---- Retrieve cluster informations and store them in ngx.shared.DICT +--- Load the cluster topology. +-- Iterate over the given `contact_points` and connects to the first one available +-- to load the cluster topology. All peers of the chosen contact point will be +-- retrieved and stored locally so that the load balancing policy can chose one +-- on each request that will be executed. +-- +-- Use this function if you want to retrieve the cluster topology sooner than when +-- you will create your first `Session`. For example: +-- +-- init_worker_by_lua_block { +-- local cassandra = require "cassandra" +-- local cluster, err = cassandra.spawn_cluster { +-- shm = "cassandra", +-- contact_points = {"127.0.0.1"} +-- } +-- } +-- +-- access_by_lua_block { +-- local cassandra = require "cassandra" +-- -- The cluster topology is already loaded at this point, +-- -- avoiding latency on your first request. +-- local session, err = cassandra.spawn_session { +-- shm = "cassandra" +-- } +-- } +-- +-- @param[type=table] options The cluster's options, including the shared dict name and the contact points. +-- @treturn boolean `ok`: Success of the cluster topology retrieval. If false, a second value will be returned describing the error. +-- @treturn table `error`: An error in case the operation did not succeed. function Cassandra.spawn_cluster(options) local cluster_options, err = opts.parse_cluster(options) if err then - return nil, err + return false, err end local addresses, err = Cassandra.refresh_hosts(cluster_options) if addresses == nil then - return nil, err + return false, err end - return setmetatable({options = cluster_options}, Cluster) + return true end ---- Cassandra Misc --- @section cassandra_misc +--- Type serializer shorthands. +-- When binding parameters to a query from `execute`, some +-- types cannot be infered automatically and will require manual +-- serialization. Some other times, it can be useful to manually enforce +-- the type of a parameter. +-- +-- See the [Cassandra Data Types](http://docs.datastax.com/en/cql/3.1/cql/cql_reference/cql_data_types_c.html). +-- +-- For this purpose, shorthands for type serialization are available +-- on the `Cassandra` table: +-- +-- @field uuid Serialize a 32 lowercase characters string to a uuid +-- cassandra.uuid("123e4567-e89b-12d3-a456-426655440000") +-- @field timestamp Serialize a 10 digits number into a Cassandra timestamp +-- cassandra.timestamp(1405356926) +-- @field list +-- cassandra.list({"abc", "def"}) +-- @field map +-- cassandra.map({foo = "bar"}) +-- @field set +-- cassandra.set({foo = "bar"}) +-- @field udt +-- @field tuple +-- @field inet +-- cassandra.inet("127.0.0.1") +-- cassandra.inet("2001:0db8:85a3:0042:1000:8a2e:0370:7334") +-- @field bigint +-- cassandra.bigint(42000000000) +-- @field double +-- cassandra.bigint(1.0000000000000004) +-- @field ascii +-- @field blob +-- @field boolean +-- @field counter +-- @field decimal +-- @field float +-- @field int +-- @field text +-- @field timeuuid +-- @field varchar +-- @field varint +-- @table type_serializers local CQL_TYPES = types.cql_types - local types_mt = {} function types_mt:__index(key) diff --git a/src/cassandra/log.lua b/src/cassandra/log.lua index 21efd42..694a0ae 100644 --- a/src/cassandra/log.lua +++ b/src/cassandra/log.lua @@ -1,4 +1,4 @@ ---- Logging wrapper +-- Logging wrapper -- lua-cassandra is built with support for pure Lua, outside of ngx_lua, -- this module provides a fallback to `print` when lua-cassandra runs -- outside of ngx_lua. From a7f321a98f70053aa199e7b2d4a2d255768e942c Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Fri, 11 Dec 2015 13:17:52 -0800 Subject: [PATCH 69/78] fix(resty/specs) catch resty.lock require --- spec/integration/cql_types_spec.lua | 3 ++- spec/integration/error_handling_spec.lua | 14 ++++++++------ src/cassandra.lua | 13 ++++++++----- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/spec/integration/cql_types_spec.lua b/spec/integration/cql_types_spec.lua index eaa69a9..3a8a5c7 100644 --- a/spec/integration/cql_types_spec.lua +++ b/spec/integration/cql_types_spec.lua @@ -12,7 +12,8 @@ describe("CQL types integration", function() local session setup(function() - local session, err = cluster:spawn_session { + local err + session, err = cassandra.spawn_session { shm = _shm, contact_points = _hosts } diff --git a/spec/integration/error_handling_spec.lua b/spec/integration/error_handling_spec.lua index c637cc6..ecfb5ae 100644 --- a/spec/integration/error_handling_spec.lua +++ b/spec/integration/error_handling_spec.lua @@ -86,14 +86,12 @@ describe("error handling", function() local session setup(function() - local cluster, err = cassandra.spawn_cluster { + local err + session, err = cassandra.spawn_session { shm = _shm, contact_points = _hosts } assert.falsy(err) - - session, err = cluster:spawn_session {shm = _shm} - assert.falsy(err) end) teardown(function() session:shutdown() @@ -117,11 +115,12 @@ describe("error handling", function() local dict = cache.get_dict(shm) assert.truthy(dict) - local cluster, err = cassandra.spawn_cluster { + local ok, err = cassandra.spawn_cluster { shm = shm, contact_points = _hosts } assert.falsy(err) + assert.True(ok) assert.truthy(cache.get_hosts(shm)) -- erase hosts from the cache @@ -129,7 +128,10 @@ describe("error handling", function() assert.falsy(cache.get_hosts(shm)) -- attempt session create - local session, err = cluster:spawn_session() + local session, err = cassandra.spawn_session { + shm = shm, + contact_points = _hosts + } assert.falsy(err) -- attempt query diff --git a/src/cassandra.lua b/src/cassandra.lua index dc8cfb2..d13229b 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -18,17 +18,20 @@ local string_utils = require "cassandra.utils.string" local FrameHeader = require "cassandra.types.frame_header" local FrameReader = require "cassandra.frame_reader" +local resty_lock +local status, res = pcall(require, "resty.lock") +if status then + resty_lock = res +end + local CQL_Errors = types.ERRORS local string_find = string.find local table_insert = table.insert local string_format = string.format local setmetatable = setmetatable -local is_ngx = ngx ~= nil - local function lock_mutex(shm, key) - if is_ngx then - local resty_lock = require "resty.lock" + if resty_lock then local lock = resty_lock:new(shm) local elapsed, err = lock:lock(key) if err then @@ -41,7 +44,7 @@ local function lock_mutex(shm, key) end local function unlock_mutex(lock) - if is_ngx then + if resty_lock then local ok, err = lock:unlock() if not ok then err = "Error unlocking mutex: "..err From ac4fc02156d2f33c30b280d0f980772706844270 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Fri, 11 Dec 2015 17:06:46 -0800 Subject: [PATCH 70/78] feat(rewrite) allow to set log lvl and format from cassandra --- .luacheckrc | 2 +- spec/integration/cassandra_spec.lua | 14 +++--- spec/integration/cql_types_spec.lua | 2 +- spec/integration/error_handling_spec.lua | 2 +- spec/spec_utils.lua | 5 -- spec/unit/cassandra_spec.lua | 60 ++++++++++++++++++++++++ src/cassandra.lua | 10 +++- src/cassandra/log.lua | 20 ++++++-- 8 files changed, 95 insertions(+), 20 deletions(-) create mode 100644 spec/unit/cassandra_spec.lua diff --git a/.luacheckrc b/.luacheckrc index efffb5d..908d580 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -1,3 +1,3 @@ unused_args = false redefined = false -globals = {"ngx", "describe", "setup", "teardown", "it", "pending", "before_each", "after_each", "finally", "spy"} +globals = {"ngx", "describe", "setup", "teardown", "it", "pending", "before_each", "after_each", "finally", "spy", "mock"} diff --git a/spec/integration/cassandra_spec.lua b/spec/integration/cassandra_spec.lua index f766ac5..e25f355 100644 --- a/spec/integration/cassandra_spec.lua +++ b/spec/integration/cassandra_spec.lua @@ -9,7 +9,7 @@ local cassandra = require "cassandra" local LOG_LVL = "ERR" -- Define log level for tests -utils.set_log_lvl(LOG_LVL) +cassandra.set_log_level(LOG_LVL) local _shm = "cassandra_specs" local _hosts = utils.hosts @@ -36,9 +36,9 @@ describe("spawn_cluster()", function() end end) it("should iterate over contact_points to find an entrance into the cluster", function() - utils.set_log_lvl("QUIET") + cassandra.set_log_level("QUIET") finally(function() - utils.set_log_lvl(LOG_LVL) + cassandra.set_log_level(LOG_LVL) end) local contact_points = {"0.0.0.1", "0.0.0.2", "0.0.0.3"} @@ -52,9 +52,9 @@ describe("spawn_cluster()", function() assert.True(ok) end) it("should accept a custom port for given hosts", function() - utils.set_log_lvl("QUIET") + cassandra.set_log_level("QUIET") finally(function() - utils.set_log_lvl(LOG_LVL) + cassandra.set_log_level(LOG_LVL) end) local contact_points = {} @@ -70,9 +70,9 @@ describe("spawn_cluster()", function() assert.equal("NoHostAvailableError", err.type) end) it("should accept a custom port through an option", function() - utils.set_log_lvl("QUIET") + cassandra.set_log_level("QUIET") finally(function() - utils.set_log_lvl(LOG_LVL) + cassandra.set_log_level(LOG_LVL) end) local ok, err = cassandra.spawn_cluster({ diff --git a/spec/integration/cql_types_spec.lua b/spec/integration/cql_types_spec.lua index 3a8a5c7..19a1a20 100644 --- a/spec/integration/cql_types_spec.lua +++ b/spec/integration/cql_types_spec.lua @@ -6,7 +6,7 @@ local _hosts = utils.hosts local _keyspace = "resty_cassandra_cql_types_specs" -- Define log level for tests -utils.set_log_lvl("ERR") +cassandra.set_log_level("ERR") describe("CQL types integration", function() local session diff --git a/spec/integration/error_handling_spec.lua b/spec/integration/error_handling_spec.lua index ecfb5ae..c41fefc 100644 --- a/spec/integration/error_handling_spec.lua +++ b/spec/integration/error_handling_spec.lua @@ -4,7 +4,7 @@ local cassandra = require "cassandra" local LOG_LVL = "ERR" -- Define log level for tests -utils.set_log_lvl(LOG_LVL) +cassandra.set_log_level(LOG_LVL) local _shm = "cassandra_error_specs" local _hosts = utils.hosts diff --git a/spec/spec_utils.lua b/spec/spec_utils.lua index ed306fc..b09cbb1 100644 --- a/spec/spec_utils.lua +++ b/spec/spec_utils.lua @@ -1,5 +1,4 @@ local say = require "say" -local log = require "cassandra.log" local types = require "cassandra.types" local assert = require "luassert.assert" local string_utils = require "cassandra.utils.string" @@ -13,10 +12,6 @@ end local _M = {} -function _M.set_log_lvl(lvl) - log.set_lvl(lvl) -end - function _M.create_keyspace(session, keyspace) local res, err = session:execute([[ CREATE KEYSPACE IF NOT EXISTS ]]..keyspace..[[ diff --git a/spec/unit/cassandra_spec.lua b/spec/unit/cassandra_spec.lua new file mode 100644 index 0000000..15384e4 --- /dev/null +++ b/spec/unit/cassandra_spec.lua @@ -0,0 +1,60 @@ +local log = require "cassandra.log" +local cassandra = require "cassandra" + +describe("Casandra", function() + local p = log.print + setup(function() + spy.on(log, "set_lvl") + local l = mock(log.print, true) + log.print = l + end) + teardown(function() + log.set_lvl:revert() + log.print = p + end) + it("should have a default logging level", function() + local lvl = log.get_lvl() + assert.equal(3, lvl) + end) + it("should have a default format", function() + finally(function() + log.print:clear() + end) + log.err("hello") + assert.spy(log.print).was.called_with("ERR -- hello") + end) + describe("set_log_level", function() + it("should set the logging level when outside of ngx_lua", function() + finally(function() + log.print:clear() + end) + + cassandra.set_log_level("INFO") + assert.spy(log.set_lvl).was.called_with("INFO") + + -- INFO + log.err("hello world") + log.info("hello world") + assert.spy(log.print).was.called(2) + + log.print:clear() + cassandra.set_log_level("ERR") + + -- ERR + log.err("bye world") + log.info("bye world") + assert.spy(log.print).was.called(1) + end) + end) + describe("set_log_format", function() + it("should set the logging format when outside of ngx_lua", function() + finally(function() + log.print:clear() + end) + + cassandra.set_log_format("Cassandra [%s]: %s") + log.err("some error") + assert.spy(log.print).was.called_with("Cassandra [ERR]: some error") + end) + end) +end) diff --git a/src/cassandra.lua b/src/cassandra.lua index d13229b..c3aeb6f 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -566,7 +566,7 @@ function RequestHandler:send_on_next_coordinator(request) return nil, err end - log.info("Acquired connection through load balancing policy: "..coordinator.address) + log.debug("Acquired connection through load balancing policy: "..coordinator.address) return self:send(request) end @@ -1091,6 +1091,14 @@ function Cassandra.spawn_cluster(options) return true end +function Cassandra.set_log_level(lvl) + log.set_lvl(lvl) +end + +function Cassandra.set_log_format(fmt) + log.set_format(fmt) +end + --- Type serializer shorthands. -- When binding parameters to a query from `execute`, some -- types cannot be infered automatically and will require manual diff --git a/src/cassandra/log.lua b/src/cassandra/log.lua index 694a0ae..c6ad1ec 100644 --- a/src/cassandra/log.lua +++ b/src/cassandra/log.lua @@ -9,8 +9,7 @@ local ngx_get_phase = is_ngx and ngx.get_phase local string_format = string.format local print = print --- ngx_lua levels redefinition for helpers and --- when outside of ngx_lua. +-- ngx_lua levels redefinition when outside of ngx_lua. local LEVELS = { QUIET = 0, ERR = 1, @@ -21,22 +20,35 @@ local LEVELS = { -- Default logging level when outside of ngx_lua. local cur_lvl = LEVELS.INFO +local cur_fmt = "%s -- %s" local log = {} function log.set_lvl(lvl_name) - if is_ngx then return end if LEVELS[lvl_name] ~= nil then cur_lvl = LEVELS[lvl_name] end end +function log.get_lvl() + return cur_lvl +end + +function log.set_format(fmt) + cur_fmt = fmt +end + +-- Makes this module testable by spying on this function +function log.print(str) + print(str) +end + for lvl_name, lvl in pairs(LEVELS) do log[lvl_name:lower()] = function(...) if is_ngx and ngx_get_phase() ~= "init" then ngx_log(ngx[lvl_name], ...) elseif lvl <= cur_lvl then - print(string_format("%s -- %s", lvl_name, ...)) + log.print(string_format(cur_fmt, lvl_name, ...)) end end end From 4a56056bdc42850d0251f68c902bc82af18ec958 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Fri, 11 Dec 2015 19:54:14 -0800 Subject: [PATCH 71/78] feat(rewrite) set_keep_alive() fallback to close() --- spec/integration/cassandra_spec.lua | 21 +++++++++++++++++++-- spec/integration/error_handling_spec.lua | 4 ++-- src/cassandra.lua | 14 +++++++++++--- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/spec/integration/cassandra_spec.lua b/spec/integration/cassandra_spec.lua index e25f355..75f087e 100644 --- a/spec/integration/cassandra_spec.lua +++ b/spec/integration/cassandra_spec.lua @@ -90,11 +90,11 @@ describe("spawn_session()", function() local session it("should spawn a session", function() local err - session, err = cassandra.spawn_session({shm = _shm}) + session, err = cassandra.spawn_session {shm = _shm} assert.falsy(err) assert.truthy(session) assert.truthy(session.hosts) - assert.equal(3, #session.hosts) + assert.equal(#_hosts, #session.hosts) end) it("should spawn a session without having to spawn a cluster", function() local shm = "session_without_cluster" @@ -567,4 +567,21 @@ describe("session", function() assert.falsy(rows) end) end) + + describe("set_keep_alive()", function() + it("should fallback to close() when outside of ngx_lua", function() + local session, err = cassandra.spawn_session { + shm = _shm, + contact_points = _hosts + } + assert.falsy(err) + + local _, err = session:execute("SELECT * FROM system.local") + assert.falsy(err) + + assert.has_no_error(function() + session:set_keep_alive() + end) + end) + end) end) diff --git a/spec/integration/error_handling_spec.lua b/spec/integration/error_handling_spec.lua index c41fefc..ea16af3 100644 --- a/spec/integration/error_handling_spec.lua +++ b/spec/integration/error_handling_spec.lua @@ -32,9 +32,9 @@ describe("error handling", function() assert.equal("shm must be a valid string", err) end) it("should return an error when no contact_point is valid", function() - utils.set_log_lvl("QUIET") + cassandra.set_log_level("QUIET") finally(function() - utils.set_log_lvl(LOG_LVL) + cassandra.set_log_level(LOG_LVL) end) local contact_points = {"0.0.0.1", "0.0.0.2", "0.0.0.3"} diff --git a/src/cassandra.lua b/src/cassandra.lua index c3aeb6f..66a1fbb 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -29,6 +29,8 @@ local string_find = string.find local table_insert = table.insert local string_format = string.format local setmetatable = setmetatable +local ipairs = ipairs +local pairs = pairs local function lock_mutex(shm, key) if resty_lock then @@ -66,7 +68,6 @@ local Cassandra = { MIN_PROTOCOL_VERSION = MIN_PROTOCOL_VERSION } - --- Host -- A connection to a single host. -- Not cluster aware, only maintain a socket to its peer. @@ -351,6 +352,10 @@ function Host:get_reused_times() return 0 end +function Host:can_keep_alive() + return self.socket_type == "ngx" +end + function Host:set_keep_alive() -- don't close if the connection was not opened yet if not self.connected then @@ -381,7 +386,6 @@ function Host:close() log.err("Could not close socket to "..self.address..". "..err) return false, Errors.SocketError(self.address, err) end - self.connected = false return true end @@ -903,7 +907,11 @@ end function Session:set_keep_alive() for _, host in ipairs(self.hosts) do - host:set_keep_alive() + if host:can_keep_alive() then + host:set_keep_alive() + else + host:close() + end end end From b6fc8ea50a363309f45d3bbcb178f571a5c10ffb Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Mon, 14 Dec 2015 12:45:55 -0800 Subject: [PATCH 72/78] feat(ssl) new enabled property for SSL options. Allow to enable SSL without having to necessarily verify the server certificate. --- spec/unit/options_spec.lua | 15 ++++++++ spec/unit/utils_spec.lua | 10 ++++++ src/cassandra.lua | 67 +++++++++++++++++------------------ src/cassandra/options.lua | 23 ++++++++---- src/cassandra/utils/table.lua | 6 ++-- 5 files changed, 77 insertions(+), 44 deletions(-) diff --git a/spec/unit/options_spec.lua b/spec/unit/options_spec.lua index f616808..5fde4c5 100644 --- a/spec/unit/options_spec.lua +++ b/spec/unit/options_spec.lua @@ -107,6 +107,21 @@ describe("options parsing", function() })) assert.equal("socket read_timeout must be a number", err) end) + it("should validate SSL options", function() + local err = select(2, parse_session { + shm = "test", + ssl_options = "" + }) + assert.equal("ssl_options must be a table", err) + + err = select(2, parse_session { + shm = "test", + ssl_options = { + enabled = "" + } + }) + assert.equal("ssl_options.enabled must be a boolean", err) + end) it("should set `prepared_shm` to `shm` if nil", function() local options, err = parse_session { shm = "test" diff --git a/spec/unit/utils_spec.lua b/spec/unit/utils_spec.lua index 2c7d5e9..3c1d739 100644 --- a/spec/unit/utils_spec.lua +++ b/spec/unit/utils_spec.lua @@ -61,5 +61,15 @@ describe("table_utils", function() target = table_utils.extend_table(source, target) assert.False(target.source) end) + it("should ignore targets that are not tables", function() + local source = {foo = {bar = "foobar"}} + local target = {foo = "hello"} + + assert.has_no_error(function() + target = table_utils.extend_table(source, target) + end) + + assert.equal("hello", target.foo) + end) end) end) diff --git a/src/cassandra.lua b/src/cassandra.lua index 66a1fbb..1eef282 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -101,17 +101,14 @@ function Host:new(address, options) local host, port = string_utils.split_by_colon(address) if not port then port = options.protocol_options.default_port end - local h = {} - - h.host = host - h.port = port - h.address = address - h.protocol_version = DEFAULT_PROTOCOL_VERSION - - h.options = options - h.reconnection_policy = h.options.policies.reconnection - - new_socket(h) + local h = { + host = host, + port = port, + address = address, + protocol_version = DEFAULT_PROTOCOL_VERSION, + options = options, + reconnection_policy = options.policy.reconnection + } return setmetatable(h, Host) end @@ -242,6 +239,8 @@ end function Host:connect() if self.connected then return true end + new_socket(self) + log.debug("Connecting to "..self.address) self:set_timeout(self.options.socket_options.connect_timeout) @@ -252,7 +251,7 @@ function Host:connect() return false, Errors.SocketError(self.address, err), true end - if self.options.ssl_options ~= nil then + if self.options.ssl_options.enabled then ok, err = do_ssl_handshake(self) if not ok then return false, Errors.SocketError(self.address, err) @@ -391,34 +390,30 @@ function Host:close() end function Host:set_down() - local host_infos, err = cache.get_host(self.options.shm, self.address) - if err then - return err + local lock, lock_err, elapsed = lock_mutex(self.options.shm, "downing_"..self.address) + if lock_err then + return lock_err end - if host_infos.unhealthy_at == 0 then - local lock, lock_err, elapsed = lock_mutex(self.options.shm, "downing_"..self.address) - if lock_err then - return lock_err - end - - if elapsed and elapsed == 0 then - log.warn("Setting host "..self.address.." as DOWN") - host_infos.unhealthy_at = time_utils.get_time() - host_infos.reconnection_delay = self.reconnection_policy.next(self) - self:close() - new_socket(self) - local ok, err = cache.set_host(self.options.shm, self.address, host_infos) - if not ok then - return err - end + if elapsed and elapsed == 0 then + local host_infos, err = cache.get_host(self.options.shm, self.address) + if err then + return err end - - lock_err = unlock_mutex(lock) - if lock_err then + log.warn("Setting host "..self.address.." as DOWN") + host_infos.unhealthy_at = time_utils.get_time() + host_infos.reconnection_delay = self.reconnection_policy.next(self) + self:close() + local ok, err = cache.set_host(self.options.shm, self.address, host_infos) + if not ok then return err end end + + lock_err = unlock_mutex(lock) + if lock_err then + return lock_err + end end function Host:set_up() @@ -458,7 +453,9 @@ function Host:can_be_considered_up() return nil, err end - return is_up or (time_utils.get_time() - host_infos.unhealthy_at >= host_infos.reconnection_delay) + if is_up or (time_utils.get_time() - host_infos.unhealthy_at >= host_infos.reconnection_delay) then + return true + end end --- Request Handler diff --git a/src/cassandra/options.lua b/src/cassandra/options.lua index e65bdc9..6660474 100644 --- a/src/cassandra/options.lua +++ b/src/cassandra/options.lua @@ -33,15 +33,16 @@ local DEFAULTS = { socket_options = { connect_timeout = 1000, read_timeout = 2000 - } + }, -- username = nil, -- password = nil, - -- ssl_options = { - -- key = nil, - -- certificate = nil, - -- ca = nil, -- stub - -- verify = false - -- } + ssl_options = { + enabled = false + -- key = nil, + -- certificate = nil, + -- ca = nil, -- stub + -- verify = false + } } local function parse_session(options, lvl) @@ -112,6 +113,14 @@ local function parse_session(options, lvl) return nil, "socket read_timeout must be a number" end + if type(options.ssl_options) ~= "table" then + return nil, "ssl_options must be a table" + end + + if type(options.ssl_options.enabled) ~= "boolean" then + return nil, "ssl_options.enabled must be a boolean" + end + return options end diff --git a/src/cassandra/utils/table.lua b/src/cassandra/utils/table.lua index c4572d1..bdfce56 100644 --- a/src/cassandra/utils/table.lua +++ b/src/cassandra/utils/table.lua @@ -1,6 +1,8 @@ local setmetatable = setmetatable local getmetatable = getmetatable +local table_remove = table.remove local tostring = tostring +local ipairs = ipairs local pairs = pairs local type = type @@ -8,14 +10,14 @@ local _M = {} function _M.extend_table(...) local sources = {...} - local values = table.remove(sources) + local values = table_remove(sources) for _, source in ipairs(sources) do for k in pairs(source) do if values[k] == nil then values[k] = source[k] end - if type(source[k]) == "table" then + if type(source[k]) == "table" and type(values[k]) == "table" then _M.extend_table(source[k], values[k]) end end From 5503e06dee612444e3b1eed256109e12b3247635 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Mon, 14 Dec 2015 15:43:26 -0800 Subject: [PATCH 73/78] fix(rewrite) typo + descriptive LuaSocket error --- src/cassandra.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cassandra.lua b/src/cassandra.lua index 1eef282..81651ab 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -90,7 +90,7 @@ local function new_socket(self) local socket, err = tcp_sock() if not socket then - error(err) + error("Could not create socket: "..err) end self.socket = socket @@ -107,7 +107,7 @@ function Host:new(address, options) address = address, protocol_version = DEFAULT_PROTOCOL_VERSION, options = options, - reconnection_policy = options.policy.reconnection + reconnection_policy = options.policies.reconnection } return setmetatable(h, Host) From 7b24d12a79743a40d9c27253831671328e5d1c9e Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Mon, 14 Dec 2015 17:44:12 -0800 Subject: [PATCH 74/78] fix(retry) always throw on unavailable + better logs --- spec/integration/cassandra_spec.lua | 8 +++++++- src/cassandra.lua | 12 ++++++------ src/cassandra/options.lua | 4 ++-- src/cassandra/policies/retry.lua | 2 +- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/spec/integration/cassandra_spec.lua b/spec/integration/cassandra_spec.lua index 75f087e..1cf60f8 100644 --- a/spec/integration/cassandra_spec.lua +++ b/spec/integration/cassandra_spec.lua @@ -116,11 +116,17 @@ describe("spawn_session()", function() assert.truthy(host_details) end end) - describe(":execute()", function() + describe("execute()", function() teardown(function() -- drop keyspace in case tests failed session:execute("DROP KEYSPACE resty_cassandra_spec_parsing") end) + it("should require #arg1 to be a string", function() + local res, err = session:execute() + assert.falsy(res) + assert.truthy(err) + assert.equal("DriverError: execute() #arg1 must be a string.", tostring(err)) + end) it("should parse ROWS results", function() local rows, err = session:execute("SELECT key FROM system.local") assert.falsy(err) diff --git a/src/cassandra.lua b/src/cassandra.lua index 81651ab..12c436e 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -400,9 +400,9 @@ function Host:set_down() if err then return err end - log.warn("Setting host "..self.address.." as DOWN") host_infos.unhealthy_at = time_utils.get_time() host_infos.reconnection_delay = self.reconnection_policy.next(self) + log.warn("Setting host "..self.address.." as DOWN. Next retry in: "..host_infos.reconnection_delay.."ms.") self:close() local ok, err = cache.set_host(self.options.shm, self.address, host_infos) if not ok then @@ -453,9 +453,7 @@ function Host:can_be_considered_up() return nil, err end - if is_up or (time_utils.get_time() - host_infos.unhealthy_at >= host_infos.reconnection_delay) then - return true - end + return is_up or (time_utils.get_time() - host_infos.unhealthy_at >= host_infos.reconnection_delay) end --- Request Handler @@ -567,7 +565,7 @@ function RequestHandler:send_on_next_coordinator(request) return nil, err end - log.debug("Acquired connection through load balancing policy: "..coordinator.address) + log.debug("Load balancing policy proposed to try host at: "..coordinator.address) return self:send(request) end @@ -665,7 +663,7 @@ function RequestHandler:prepare_and_retry(request) if err then return nil, err end - log.info("Query prepared for host "..self.coordinator.address) + log.info("Prepared query for host "..self.coordinator.address) if request.query_id ~= res.query_id then log.warn(string_format("Unexpected difference between prepared query ids for query %s (%s ~= %s)", request.query, request.query_id, res.query_id)) @@ -849,6 +847,8 @@ end function Session:execute(query, args, query_options) if self.terminated then return nil, Errors.NoHostAvailableError("Cannot reuse a session that has been shut down.") + elseif type(query) ~= "string" then + return nil, Errors.DriverError("execute() #arg1 must be a string.") end local options = table_utils.deep_copy(self.options) diff --git a/src/cassandra/options.lua b/src/cassandra/options.lua index 6660474..51d54e8 100644 --- a/src/cassandra/options.lua +++ b/src/cassandra/options.lua @@ -31,8 +31,8 @@ local DEFAULTS = { max_schema_consensus_wait = 5000 }, socket_options = { - connect_timeout = 1000, - read_timeout = 2000 + connect_timeout = 1000, -- ms + read_timeout = 2000 -- ms }, -- username = nil, -- password = nil, diff --git a/src/cassandra/policies/retry.lua b/src/cassandra/policies/retry.lua index a039d81..97b5df8 100644 --- a/src/cassandra/policies/retry.lua +++ b/src/cassandra/policies/retry.lua @@ -4,7 +4,7 @@ local DECISIONS = { } local function on_unavailable(request_infos) - return DECISIONS.retry + return DECISIONS.throw end local function on_read_timeout(request_infos) From e4f7802cadfe96751889afb085f36992f3c851b6 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Mon, 14 Dec 2015 18:41:20 -0800 Subject: [PATCH 75/78] feat(rewrite) expose consistencies and CQL errors --- spec/integration/cassandra_spec.lua | 9 ++++----- spec/unit/cassandra_spec.lua | 20 +++++++++++++++++++ src/cassandra.lua | 30 ++++++++++++++--------------- 3 files changed, 39 insertions(+), 20 deletions(-) diff --git a/spec/integration/cassandra_spec.lua b/spec/integration/cassandra_spec.lua index 1cf60f8..dd69650 100644 --- a/spec/integration/cassandra_spec.lua +++ b/spec/integration/cassandra_spec.lua @@ -121,11 +121,10 @@ describe("spawn_session()", function() -- drop keyspace in case tests failed session:execute("DROP KEYSPACE resty_cassandra_spec_parsing") end) - it("should require #arg1 to be a string", function() - local res, err = session:execute() - assert.falsy(res) - assert.truthy(err) - assert.equal("DriverError: execute() #arg1 must be a string.", tostring(err)) + it("should require argument #1 to be a string", function() + assert.has_error(function() + session:execute() + end, "argument #1 must be a string") end) it("should parse ROWS results", function() local rows, err = session:execute("SELECT key FROM system.local") diff --git a/spec/unit/cassandra_spec.lua b/spec/unit/cassandra_spec.lua index 15384e4..a357cd4 100644 --- a/spec/unit/cassandra_spec.lua +++ b/spec/unit/cassandra_spec.lua @@ -57,4 +57,24 @@ describe("Casandra", function() assert.spy(log.print).was.called_with("Cassandra [ERR]: some error") end) end) + describe("consistencies", function() + it("should have Cassandra data consistency values available", function() + assert.truthy(cassandra.consistencies) + + local types = require "cassandra.types" + for t in pairs(types.consistencies) do + assert.truthy(cassandra.consistencies[t]) + end + end) + end) + describe("cql_errors", function() + it("should have Cassandra CQL error types values available", function() + assert.truthy(cassandra.cql_errors) + + local types = require "cassandra.types" + for t in pairs(types.ERRORS) do + assert.truthy(cassandra.cql_errors[t]) + end + end) + end) end) diff --git a/src/cassandra.lua b/src/cassandra.lua index 12c436e..910f35a 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -848,7 +848,7 @@ function Session:execute(query, args, query_options) if self.terminated then return nil, Errors.NoHostAvailableError("Cannot reuse a session that has been shut down.") elseif type(query) ~= "string" then - return nil, Errors.DriverError("execute() #arg1 must be a string.") + error("argument #1 must be a string", 2) end local options = table_utils.deep_copy(self.options) @@ -1147,27 +1147,27 @@ end -- @field varint -- @table type_serializers -local CQL_TYPES = types.cql_types -local types_mt = {} +local types_mt = { + __index = function(self, key) + if types.cql_types[key] ~= nil then + return function(value) + if value == nil then + error("argument #1 required for '"..key.."' type shorthand", 2) + end -function types_mt:__index(key) - if CQL_TYPES[key] ~= nil then - return function(value) - if value == nil then - error("argument #1 required for '"..key.."' type shorthand", 2) + return {value = value, type_id = types.cql_types[key]} end - - return {value = value, type_id = CQL_TYPES[key]} + elseif key == "unset" then + return {value = "unset", type_id = "unset"} end - elseif key == "unset" then - return {value = "unset", type_id = "unset"} - end - return rawget(self, key) -end + return rawget(self, key) + end +} setmetatable(Cassandra, types_mt) Cassandra.consistencies = types.consistencies +Cassandra.cql_errors = types.ERRORS return Cassandra From 759289d7404ff47a24779ee1c554861648e05adb Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Mon, 14 Dec 2015 19:13:34 -0800 Subject: [PATCH 76/78] feat(rewrite) options for cosockets' setkeepalive() --- spec/unit/options_spec.lua | 22 ++++++++++++++++++++++ src/cassandra.lua | 2 +- src/cassandra/options.lua | 16 ++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/spec/unit/options_spec.lua b/spec/unit/options_spec.lua index 5fde4c5..6dcad72 100644 --- a/spec/unit/options_spec.lua +++ b/spec/unit/options_spec.lua @@ -92,6 +92,12 @@ describe("options parsing", function() end) it("should validate socket options", function() local err = select(2, parse_session({ + shm = "test", + socket_options = "" + })) + assert.equal("socket_options must be a table", err) + + err = select(2, parse_session({ shm = "test", socket_options = { connect_timeout = "" @@ -106,6 +112,22 @@ describe("options parsing", function() } })) assert.equal("socket read_timeout must be a number", err) + + err = select(2, parse_session({ + shm = "test", + socket_options = { + pool_timeout = "" + } + })) + assert.equal("socket pool_timeout must be a number", err) + + err = select(2, parse_session({ + shm = "test", + socket_options = { + pool_size = "" + } + })) + assert.equal("socket pool_size must be a number", err) end) it("should validate SSL options", function() local err = select(2, parse_session { diff --git a/src/cassandra.lua b/src/cassandra.lua index 910f35a..600b5e2 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -362,7 +362,7 @@ function Host:set_keep_alive() end if self.socket_type == "ngx" then - local ok, err = self.socket:setkeepalive() + local ok, err = self.socket:setkeepalive(self.options.socket_options.pool_timeout, self.options.socket_options.pool_size) if err then log.err("Could not set keepalive socket to "..self.address..". "..err) return ok, Errors.SocketError(self.address, err) diff --git a/src/cassandra/options.lua b/src/cassandra/options.lua index 51d54e8..2c78862 100644 --- a/src/cassandra/options.lua +++ b/src/cassandra/options.lua @@ -33,6 +33,8 @@ local DEFAULTS = { socket_options = { connect_timeout = 1000, -- ms read_timeout = 2000 -- ms + -- pool_timeout = nil, + -- pool_size = nil }, -- username = nil, -- password = nil, @@ -105,6 +107,10 @@ local function parse_session(options, lvl) -- socket options + if type(options.socket_options) ~= "table" then + return nil, "socket_options must be a table" + end + if type(options.socket_options.connect_timeout) ~= "number" then return nil, "socket connect_timeout must be a number" end @@ -113,6 +119,16 @@ local function parse_session(options, lvl) return nil, "socket read_timeout must be a number" end + if options.socket_options.pool_timeout ~= nil and type(options.socket_options.pool_timeout) ~= "number" then + return nil, "socket pool_timeout must be a number" + end + + if options.socket_options.pool_size ~= nil and type(options.socket_options.pool_size) ~= "number" then + return nil, "socket pool_size must be a number" + end + + -- ssl options + if type(options.ssl_options) ~= "table" then return nil, "ssl_options must be a table" end From b17b961789c16cc659c55871361ae92aac0175a1 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Tue, 15 Dec 2015 23:08:55 -0800 Subject: [PATCH 77/78] fix(setkeepalive) handle tcpsock:setkeepalive() not handling nil args See openresty/lua-nginx-module#625 --- src/cassandra.lua | 13 ++++- t/01-cassandra.t | 127 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 138 insertions(+), 2 deletions(-) diff --git a/src/cassandra.lua b/src/cassandra.lua index 600b5e2..d9d4063 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -362,7 +362,18 @@ function Host:set_keep_alive() end if self.socket_type == "ngx" then - local ok, err = self.socket:setkeepalive(self.options.socket_options.pool_timeout, self.options.socket_options.pool_size) + -- tcpsock:setkeepalive() does not accept nil values, so this is a quick workaround + -- see https://github.com/openresty/lua-nginx-module/pull/625 + local ok, err + if self.options.socket_options.pool_timeout ~= nil then + if self.options.socket_options.pool_size ~= nil then + ok, err = self.socket:setkeepalive(self.options.socket_options.pool_timeout, self.options.socket_options.pool_size) + else + ok, err = self.socket:setkeepalive(self.options.socket_options.pool_timeout) + end + else + ok, err = self.socket:setkeepalive() + end if err then log.err("Could not set keepalive socket to "..self.address..". "..err) return ok, Errors.SocketError(self.address, err) diff --git a/t/01-cassandra.t b/t/01-cassandra.t index a4d7841..be9ac26 100644 --- a/t/01-cassandra.t +++ b/t/01-cassandra.t @@ -255,7 +255,132 @@ GET /t -=== TEST 8: session:execute() prepared query +=== TEST 8: session:set_keep_alive() with pool timeout option +--- http_config eval +"$::HttpConfig + $::SpawnCluster" +--- config + location /t { + content_by_lua ' + local cassandra = require "cassandra" + local session = cassandra.spawn_session { + shm = "cassandra", + socket_options = { + pool_timeout = 60 + } + } + local rows, err = session:execute("SELECT key FROM system.local") + if err then + ngx.log(ngx.ERR, tostring(err)) + ngx.exit(500) + end + + session:set_keep_alive() + + local rows, err = session:execute("SELECT key FROM system.local") + if err then + ngx.log(ngx.ERR, tostring(err)) + ngx.exit(500) + end + + ngx.exit(200) + '; + } +--- request +GET /t +--- response_body + +--- no_error_log +[error] + + + +=== TEST 9: session:set_keep_alive() with pool size option +--- http_config eval +"$::HttpConfig + $::SpawnCluster" +--- config + location /t { + content_by_lua ' + local cassandra = require "cassandra" + -- It should ignore it since ngx_lua cannot accept + -- a nil arg #1 + local session = cassandra.spawn_session { + shm = "cassandra", + socket_options = { + pool_size = 25 + } + } + local rows, err = session:execute("SELECT key FROM system.local") + if err then + ngx.log(ngx.ERR, tostring(err)) + ngx.exit(500) + end + + session:set_keep_alive() + + local rows, err = session:execute("SELECT key FROM system.local") + if err then + ngx.log(ngx.ERR, tostring(err)) + ngx.exit(500) + end + + ngx.exit(200) + '; + } +--- request +GET /t +--- response_body + +--- no_error_log +[error] + + + +=== TEST 10: session:set_keep_alive() with pool size and pool timeout options +--- http_config eval +"$::HttpConfig + $::SpawnCluster" +--- config + location /t { + content_by_lua ' + local cassandra = require "cassandra" + -- It should ignore it since ngx_lua cannot accept + -- a nil arg #1 + local session = cassandra.spawn_session { + shm = "cassandra", + socket_options = { + pool_timeout = 60, + pool_size = 25 + } + } + local rows, err = session:execute("SELECT key FROM system.local") + if err then + ngx.log(ngx.ERR, tostring(err)) + ngx.exit(500) + end + + session:set_keep_alive() + + local rows, err = session:execute("SELECT key FROM system.local") + if err then + ngx.log(ngx.ERR, tostring(err)) + ngx.exit(500) + end + + ngx.exit(200) + '; + } +--- request +GET /t +--- response_body + +--- no_error_log +[error] + + + +=== TEST 11: session:execute() prepared query --- http_config eval "$::HttpConfig $::SpawnCluster" From 4a230974ab6f1d5801addb0bfd7dbd8e02a3f20f Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Thu, 17 Dec 2015 16:31:19 -0800 Subject: [PATCH 78/78] fix(startup) if startup request failed, it might be bcause hose is down --- src/cassandra.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cassandra.lua b/src/cassandra.lua index d9d4063..203ce42 100644 --- a/src/cassandra.lua +++ b/src/cassandra.lua @@ -284,7 +284,7 @@ function Host:connect() end end - return false, err + return false, err, true elseif res.must_authenticate then log.info("Host at "..self.address.." required authentication") local authenticator, err = auth.new_authenticator(res.class_name, self.options)