Lua cookbook recipes
Here are contributions of Lua programs for some frequent or tricky situations.
You can execute any of these programs by copying the code into a .lua
file, and then entering
chmod +x ./{program-name}.lua and
./{program-name}.lua on the terminal.
The first line is a "hashbang":
#!/usr/bin/env tarantool
This runs Tarantool Lua application server, which should be on the execution path.
This section contains the following recipes:
Use freely.
See more recipes on Tarantool GitHub.
The standard example of a simple program.
#!/usr/bin/env tarantoolprint('Hello, World!')
Use box.once() to initialize a database (creating spaces) if this is the first time the server has been run. Then use console.start() to start interactive mode.
#!/usr/bin/env tarantool-- Configure databasebox.cfg {listen = 3313}box.once("bootstrap", function()box.schema.space.create('tweedledum')box.space.tweedledum:create_index('primary',{ type = 'TREE', parts = {1, 'unsigned'}})end)require('console').start()
Use the fio module to open, read, and close a file.
#!/usr/bin/env tarantoollocal fio = require('fio')local errno = require('errno')local f = fio.open('/tmp/xxxx.txt', {'O_RDONLY' })if not f thenerror("Failed to open file: "..errno.strerror())endlocal data = f:read(4096)f:close()print(data)
Use the fio module to open, write, and close a file.
#!/usr/bin/env tarantoollocal fio = require('fio')local errno = require('errno')local f = fio.open('/tmp/xxxx.txt', {'O_CREAT', 'O_WRONLY', 'O_APPEND'},tonumber('0666', 8))if not f thenerror("Failed to open file: "..errno.strerror())endf:write("Hello\n");f:close()
Use the LuaJIT ffi library to call a C built-in function: printf(). (For help understanding ffi, see the FFI tutorial.)
#!/usr/bin/env tarantoollocal ffi = require('ffi')ffi.cdef[[int printf(const char *format, ...);]]ffi.C.printf("Hello, %s\n", os.getenv("USER"));
Use the LuaJIT ffi library to call a C function: gettimeofday(). This delivers time with millisecond precision, unlike the time function in Tarantool's clock module.
#!/usr/bin/env tarantoollocal ffi = require('ffi')ffi.cdef[[typedef long time_t;typedef struct timeval {time_t tv_sec;time_t tv_usec;} timeval;int gettimeofday(struct timeval *t, void *tzp);]]local timeval_buf = ffi.new("timeval")local now = function()ffi.C.gettimeofday(timeval_buf, nil)return tonumber(timeval_buf.tv_sec * 1000 + (timeval_buf.tv_usec / 1000))end
Use the LuaJIT ffi library to call a C library function. (For help understanding ffi, see the FFI tutorial.)
#!/usr/bin/env tarantoollocal ffi = require("ffi")ffi.cdef[[unsigned long compressBound(unsigned long sourceLen);int compress2(uint8_t *dest, unsigned long *destLen,const uint8_t *source, unsigned long sourceLen, int level);int uncompress(uint8_t *dest, unsigned long *destLen,const uint8_t *source, unsigned long sourceLen);]]local zlib = ffi.load(ffi.os == "Windows" and "zlib1" or "z")-- Lua wrapper for compress2()local function compress(txt)local n = zlib.compressBound(#txt)local buf = ffi.new("uint8_t[?]", n)local buflen = ffi.new("unsigned long[1]", n)local res = zlib.compress2(buf, buflen, txt, #txt, 9)assert(res == 0)return ffi.string(buf, buflen[0])end-- Lua wrapper for uncompresslocal function uncompress(comp, n)local buf = ffi.new("uint8_t[?]", n)local buflen = ffi.new("unsigned long[1]", n)local res = zlib.uncompress(buf, buflen, comp, #comp)assert(res == 0)return ffi.string(buf, buflen[0])end-- Simple test code.local txt = string.rep("abcd", 1000)print("Uncompressed size: ", #txt)local c = compress(txt)print("Compressed size: ", #c)local txt2 = uncompress(c, #txt)assert(txt2 == txt)
Use the LuaJIT ffi library to access a C object via a metamethod (a method which is defined with a metatable).
#!/usr/bin/env tarantoollocal ffi = require("ffi")ffi.cdef[[typedef struct { double x, y; } point_t;]]local pointlocal mt = {__add = function(a, b) return point(a.x+b.x, a.y+b.y) end,__len = function(a) return math.sqrt(a.x*a.x + a.y*a.y) end,__index = {area = function(a) return a.x*a.x + a.y*a.y end,},}point = ffi.metatype("point_t", mt)local a = point(3, 4)print(a.x, a.y) --> 3 4print(#a) --> 5print(a:area()) --> 25local b = a + point(0.5, 8)print(#b) --> 12.5
Create Lua tables, and print them. Notice that for the 'array' table
the iterator function is ipairs(), while for the 'map' table the
iterator function is pairs(). (ipairs() is faster than pairs(), but
pairs() is recommended for map-like tables or mixed tables.) The
display will look like: "1 Apple | 2 Orange | 3 Grapefruit | 4
Banana | k3 v3 | k1 v1 | k2 v2".
#!/usr/bin/env tarantoolarray = { 'Apple', 'Orange', 'Grapefruit', 'Banana'}for k, v in ipairs(array) do print(k, v) endmap = { k1 = 'v1', k2 = 'v2', k3 = 'v3' }for k, v in pairs(map) do print(k, v) end
Use the '#' operator to get the number of items in an array-like Lua table. This operation has O(log(N)) complexity.
#!/usr/bin/env tarantoolarray = { 1, 2, 3}print(#array)
Missing elements in arrays, which Lua treats as "nil"s, cause the simple "#" operator to deliver improper results. The "print(#t)" instruction will print "4"; the "print(counter)" instruction will print "3"; the "print(max)" instruction will print "10". Other table functions, such as table.sort(), will also misbehave when "nils" are present.
#!/usr/bin/env tarantoollocal t = {}t[1] = 1t[4] = 4t[10] = 10print(#t)local counter = 0for k,v in pairs(t) do counter = counter + 1 endprint(counter)local max = 0for k,v in pairs(t) do if k > max then max = k end endprint(max)
Use explicit NULL values to avoid the problems caused by Lua's nil ==
missing value behavior. Although json.NULL == nil is true, all the
print instructions in this program will print the correct value: 10.
#!/usr/bin/env tarantoollocal json = require('json')local t = {}t[1] = 1; t[2] = json.NULL; t[3]= json.NULL;t[4] = 4; t[5] = json.NULL; t[6]= json.NULL;t[6] = 4; t[7] = json.NULL; t[8]= json.NULL;t[9] = json.NULLt[10] = 10print(#t)local counter = 0for k,v in pairs(t) do counter = counter + 1 endprint(counter)local max = 0for k,v in pairs(t) do if k > max then max = k end endprint(max)
Get the number of elements in a map-like table.
#!/usr/bin/env tarantoollocal map = { a = 10, b = 15, c = 20 }local size = 0for _ in pairs(map) do size = size + 1; endprint(size)
Use a Lua peculiarity to swap two variables without needing a third variable.
#!/usr/bin/env tarantoollocal x = 1local y = 2x, y = y, xprint(x, y)
Create a class, create a metatable for the class, create an instance of the class. Another illustration is at http://lua-users.org/wiki/LuaClassesWithMetatable.
#!/usr/bin/env tarantool-- define class objectslocal myclass_somemethod = function(self)print('test 1', self.data)endlocal myclass_someothermethod = function(self)print('test 2', self.data)endlocal myclass_tostring = function(self)return 'MyClass <'..self.data..'>'endlocal myclass_mt = {__tostring = myclass_tostring;__index = {somemethod = myclass_somemethod;someothermethod = myclass_someothermethod;}}-- create a new object of myclasslocal object = setmetatable({ data = 'data'}, myclass_mt)print(object:somemethod())print(object.data)
Activate the Lua garbage collector with the collectgarbage function.
#!/usr/bin/env tarantoolcollectgarbage('collect')
Start one fiber for producer and one fiber for consumer. Use
fiber.channel() to exchange data and synchronize.
One can tweak the channel size (ch_size in the program code) to
control the number of simultaneous tasks waiting for processing.
#!/usr/bin/env tarantoollocal fiber = require('fiber')local function consumer_loop(ch, i)-- initialize consumer synchronously or raise an error()fiber.sleep(0) -- allow fiber.create() to continuewhile true dolocal data = ch:get()if data == nil thenbreakendprint('consumed', i, data)fiber.sleep(math.random()) -- simulate some workendendlocal function producer_loop(ch, i)-- initialize consumer synchronously or raise an error()fiber.sleep(0) -- allow fiber.create() to continuewhile true dolocal data = math.random()ch:put(data)print('produced', i, data)endendlocal function start()local consumer_n = 5local producer_n = 3-- Create a channellocal ch_size = math.max(consumer_n, producer_n)local ch = fiber.channel(ch_size)-- Start consumersfor i=1, consumer_n,1 dofiber.create(consumer_loop, ch, i)end-- Start producersfor i=1, producer_n,1 dofiber.create(producer_loop, ch, i)endendstart()print('started')
Use socket.tcp_connect() to connect to a remote host via TCP. Display the connection details and the result of a GET request.
#!/usr/bin/env tarantoollocal s = require('socket').tcp_connect('google.com', 80)print(s:peer().host)print(s:peer().family)print(s:peer().type)print(s:peer().protocol)print(s:peer().port)print(s:write("GET / HTTP/1.0\r\n\r\n"))print(s:read('\r\n'))print(s:read('\r\n'))
Use socket.tcp_connect() to set up a simple TCP server, by creating a function that handles requests and echos them, and passing the function to socket.tcp_server(). This program has been used to test with 100,000 clients, with each client getting a separate fiber.
#!/usr/bin/env tarantoollocal function handler(s, peer)s:write("Welcome to test server, " .. peer.host .."\n")while true dolocal line = s:read('\n')if line == nil thenbreak -- error or eofendif not s:write("pong: "..line) thenbreak -- error or eofendendendlocal server, addr = require('socket').tcp_server('localhost', 3311, handler)
Use socket.getaddrinfo() to perform non-blocking
DNS resolution, getting both the AF_INET6 and AF_INET information for
'google.com'. This technique is not always necessary for tcp
connections because socket.tcp_connect() performs
socket.getaddrinfo under the hood, before trying to
connect to the first available address.
#!/usr/bin/env tarantoollocal s = require('socket').getaddrinfo('google.com', 'http', { type = 'SOCK_STREAM' })print('host=',s[1].host)print('family=',s[1].family)print('type=',s[1].type)print('protocol=',s[1].protocol)print('port=',s[1].port)print('host=',s[2].host)print('family=',s[2].family)print('type=',s[2].type)print('protocol=',s[2].protocol)print('port=',s[2].port)
Tarantool does not currently have a udp_server function,
therefore socket_udp_echo.lua is more complicated than
socket_tcp_echo.lua. It can be implemented with sockets and fibers.
#!/usr/bin/env tarantoollocal socket = require('socket')local errno = require('errno')local fiber = require('fiber')local function udp_server_loop(s, handler)fiber.name("udp_server")while true do-- try to read a datagram firstlocal msg, peer = s:recvfrom()if msg == "" then-- socket was closed via s:close()breakelseif msg ~= nil then-- got a new datagramhandler(s, peer, msg)elseif s:errno() == errno.EAGAIN or s:errno() == errno.EINTR then-- socket is not readys:readable() -- yield, epoll will wake us when new data arriveselse-- socket errorlocal msg = s:error()s:close() -- save resources and don't wait GCerror("Socket error: " .. msg)endendendendlocal function udp_server(host, port, handler)local s = socket('AF_INET', 'SOCK_DGRAM', 0)if not s thenreturn nil -- check errno:strerror()endif not s:bind(host, port) thenlocal e = s:errno() -- save errnos:close()errno(e) -- restore errnoreturn nil -- check errno:strerror()endfiber.create(udp_server_loop, s, handler) -- start a new background fiberreturn send
A function for a client that connects to this server could look something like this ...
local function handler(s, peer, msg)-- You don't have to wait until socket is ready to send UDP-- s:writable()s:sendto(peer.host, peer.port, "Pong: " .. msg)endlocal server = udp_server('127.0.0.1', 3548, handler)if not server thenerror('Failed to bind: ' .. errno.strerror())endprint('Started')require('console').start()
Use the http module to get data via HTTP.
#!/usr/bin/env tarantoollocal http_client = require('http.client')local json = require('json')local r = http_client.get('https://api.frankfurter.app/latest?to=USD%2CRUB')if r.status ~= 200 thenprint('Failed to get currency ', r.reason)returnendlocal data = json.decode(r.body)print(data.base, 'rate of', data.date, 'is', data.rates.RUB, 'RUB or', data.rates.USD, 'USD')
Use the http module to send data via HTTP.
#!/usr/bin/env tarantoollocal http_client = require('http.client')local json = require('json')local data = json.encode({ Key = 'Value'})local headers = { Token = 'xxxx', ['X-Secret-Value'] = '42' }local r = http_client.post('http://localhost:8081', data, { headers = headers})if r.status == 200 thenprint 'Success'end
Use the http rock (which must first be installed) to turn Tarantool into a web server.
#!/usr/bin/env tarantoollocal function handler(self)return self:render{ json = { ['Your-IP-Is'] = self.peer.host } }endlocal server = require('http.server').new(nil, 8080, {charset = "utf8"}) -- listen *:8080server:route({ path = '/' }, handler)server:start()-- connect to localhost:8080 and see json
Use the http rock
(which must first be installed) to generate HTML pages from templates.
The http
rock has a fairly simple template engine
which allows execution of regular Lua code inside text blocks (like
PHP). Therefore there is no need to learn new languages in order to
write templates.
#!/usr/bin/env tarantoollocal function handler(self)local fruits = {'Apple', 'Orange', 'Grapefruit', 'Banana'}return self:render{ fruits = fruits }endlocal server = require('http.server').new(nil, 8080, {charset = "utf8"}) -- nil means '*'server:route({ path = '/', file = 'index.html.lua' }, handler)server:start()
An "HTML" file for this server, including Lua, could look like this
(it would produce "1 Apple | 2 Orange | 3 Grapefruit | 4 Banana").
Create a templates directory and put this file in it:
<html><body></body></html>
In Go, there is no one-liner to select all tuples from a Tarantool space. Yet you can use a script like this one. Call it on the instance you want to connect to.
cookbook/main.go