Ruby Integration in Nginx
For a long time already there is a well-known bunch of Nginx + Lua, including a number of articles. But time does not stand still. About a year ago, the first version of the module integrating Ruby into Nginx appeared.
Mruby
For integration, not a full-fledged Ruby was chosen, but a subset of it, which is designed to be embedded in other applications, devices, etc. It has some limitations, but otherwise full Ruby. The project is called MRuby . Currently it already has version 1.0.0, i.e. considered stable.
MRuby does not allow you to attach other files at run time, so the whole program must be in one file. At the same time, it is possible to convert the program to bytecode and execute it already, which has a positive effect on performance.
Because there is no way to load other files, then existing gems are not suitable for it. To expand the functionality, its own format is used, which is both C code and Ruby in some places. These modules are assembled together with the library itself at compile time and are an integral part of it. There are binders to various databases for working with files, a network, and so on. A complete list is available on the site.
Also there is a module that allows you to integrate this engine into Nginx, which was especially interesting.
ngx_mruby
So get to know : ngx_mruby . Module for connecting ruby scripts to nginx. It has similar functionality with the Lua version. Allows you to perform operations at various stages of request processing.
The module is assembled quite simply, the site has detailed instructions. Those who do not want to bother with the assembly can download the finished package:
http://mruby.ajieks.ru/st/nginx_1.4.4-1~mruby~precise_amd64.deb
MRuby in this assembly contains the following additional modules:
- github.com/iij/mruby-io
- github.com/iij/mruby-env
- github.com/iij/mruby-dir
- github.com/iij/mruby-process
- github.com/iij/mruby-pack
- github.com/iij/mruby-digest
- github.com/mattn/mruby-json
- github.com/matsumoto-r/mruby-redis
- github.com/matsumoto-r/mruby-vedis
- github.com/matsumoto-r/mruby-sleep
- github.com/matsumoto-r/mruby-userdata
- github.com/matsumoto-r/mruby-uname
- github.com/mattn/mruby-onig-regexp
- github.com/matsumoto-r/mruby-discount
As you can see, there is almost everything necessary for work. The only thing that this module did not find in the API was the ability to make a request outside. Most likely, it will need to be implemented as an extension and make a binding around the nginx API.
The author shows a beautiful graph with tests, but did not find the configuration of the environment. Therefore, I just apply it for beauty:
Let's try to use
So, the server is already installed. Everything is functioning, static is given. Add a bit of dynamics to this.
As an example, I chose the task of parsing Markdown markup and rendering it in HTML without an additional server application. As well as line numbering in Ruby source.
To do this, a clone of the sinatra repository is made and nginx is configured to solve the task.
Markdown
To process the markup, we will use the mruby-discount module connected to the assembly. It provides a simple class for working with markup. It is based on the C library of the same name, because the issue of performance, I think, will not particularly stand.
To begin, write a program that will read the requested file from disk, process it and give it to the user.
r = Nginx::Request.new
m = Discount.new("/st/style.css", "README")
filename = r.filename
filename = File.join(filename, 'README.md') if filename.end_with?('/')
markdown = File.exists?(filename) ? File.read(filename) : ''
Nginx.rputs m.header
Nginx.rputs m.md2html(markdown)
Nginx.rputs m.footer
The first line is an instance of the request object containing all the necessary information, including the requested file, headers, URLs, URIs, etc.
The next line creates an instance of the Discount class, indicating the style file and page title.
This code does not handle 404 errors, so even if there is no file, there will always be a 200 return code.
Now we connect it all
location ~ \.md$ {
add_header Content-Type text/html;
mruby_content_handler "/opt/app/parse_md.rb" cache;
}
Result:
mruby.ajieks.ru/sinatra
mruby.ajieks.ru/sinatra/README.ru.md
Ruby Files
Initially, I planned to do not just numbering, but also coloring the code using the once-written code https://github.com/fuCtor/chalks . However, after all the adaptations made, problems arose in his work. The code seemed to work, but at some point it crashed with a Segmentation fault. The initial suspicion was a lack of memory allocated, but even after reducing its consumption, the problem did not disappear. After removing the code associated with the coloring, everything worked, but not as beautifully as I wanted.
Change result
module CGI
TABLE_FOR_ESCAPE_HTML__ = {"&"=>"&", '"'=>""", "<"=>"<", ">"=>">"}
def self.escapeHTML(string)
string.gsub(/[&\"<>]/) do |ch|
TABLE_FOR_ESCAPE_HTML__[ch]
end
end
end
class String
def ord
self.bytes[0]
end
end
class Chalk
COMMENT_START_CHARS = {
ruby: /#./,
cpp: /\/\*|\/\//,
c: /\/\//
}
COMMENT_END_CHARS = {
cpp: /\*\/|.\n/,
ruby: /.\n/,
c: /.\n/,
}
STRING_SEP = %w(' ")
SEPARATORS = " @(){}[],.:;\"\'`<>=+-*/\t\n\\?|&#"
SEPARATORS_RX = /[@\(\)\{\}\[\],\.\:;"'`\<\>=\+\-\*\/\t\n\\\?\|\&#]/
def initialize(file)
@filename = file
@file = File.new(file)
@rnd = Random.new(file.hash)
@tokens = {}
reset
end
def parse &block
reset()
@file.read.each_char do |char|
@last_couple = ((@last_couple.size < 2) ? @last_couple : @last_couple[1]) + char
case(@state)
when :source
if start_comment?(@last_couple)
@state = :comment
elsif STRING_SEP.include?(char)
@string_started_with = char
@state = :string
else
process_entity(&block) if (@entity.length == 1 && SEPARATORS.index(@entity)) || SEPARATORS.index(char)
end
when :comment
process_entity(:source, &block) if end_comment?(@last_couple)
when :string
if (STRING_SEP.include?(char) && @string_started_with == char)
@entity += char
process_entity(:source, &block)
char = ''
elsif char == '\\'
@state = :escaped_char
else
end
when :escaped_char
@state = :string
end
@entity += char
end
end
def to_html(&block)
html = ''
if block
block.call( '' )
else
html = '
'
end
line_n = 1
@file.readlines.each do
if block
block.call( "#{line_n}\n" )
else
html += "#{line_n}\n"
end
line_n += 1
end
@file = File.open(@filename)
if block
block.call( '
' )
else
html += '
'
end
parse do |entity, type|
entity = entity.gsub("\t", ' ')
if block
block.call( entity )
#block.call(highlight( entity , type))
else
html += entity
#html += highlight( entity , type)
end
end
if block
block.call( '
' )
else
html + '
'
end
end
def language
@language ||= case(@file.path.to_s.split('.').last.to_sym)
when :rb
:ruby
when :cpp, :hpp
:cpp
when :c, :h
:c
when :py
:python
else
@file.path.to_s.split('.').last.to_s
end
end
private
def process_entity(new_state = nil, &block)
block.call @entity, @state if block
@entity = ''
@state = new_state if new_state
end
def reset
@file = File.open(@filename) if @file
@state = :source
@string_started_with = ''
@entity = ''
@last_couple = ''
end
def color(entity)
entity = entity.strip
entity.gsub! SEPARATORS_RX, ''
token = ''
return token if entity.empty?
#return token if token = @tokens[entity]
return '' if entity[0].ord >= 128
rgb = [ @rnd.rand(150) + 100, @rnd.rand(150) + 100, @rnd.rand(150) + 100 ]
token = String.sprintf("#%02X%02X%02X", rgb[0], rgb[1], rgb[2])
#token = "#%02X%02X%02X" % rgb
#@tokens[entity] = token
return token
end
def highlight(entity, type)
esc_entity = CGI.escapeHTML( entity )
case type
when :string, :comment
"#{esc_entity}"
else
rgb = color(entity)
if rgb.empty?
esc_entity
else
"#{esc_entity}"
end
end
end
def start_comment?(char)
rx = COMMENT_START_CHARS[language]
char.match rx if rx
end
def end_comment?(char)
rx = COMMENT_END_CHARS[language]
char.match rx if rx
end
end
And actually the code that performs the file reading and numbering:
r = Nginx::Request.new
Nginx.rputs ''
begin
ch = Chalk.new(r.filename)
data = ch.to_html
Nginx.rputs data
rescue => e
Nginx.rputs e.message
end
Nginx.rputs ''
We connect everything. Because the Chalk class is used constantly, we will load it in advance:
mruby_init '/opt/app/init.rb';
This line is added before the server section in the settings. Next, we already indicate our handler:
location ~ \.rb$ {
add_header Content-Type text/html;
mruby_content_handler "/opt/app/parse_code.rb" cache;
}
That's it, now you can look at the result: mruby.ajieks.ru/sinatra/lib/sinatra/main.rb
Conclusion
Thus, it is possible to implement advanced query processing, filtering, caching, using another of the languages. Whether this module is ready for use in combat conditions, I do not know. While testing, there were freezes of the entire server, but there is a possibility of curvature of the hands, or still not everything is fully developed. I will follow the development of the project.
Those interested can run the performance scripts indicated in the article using the links above.
The server is deployed on DigitalOcean on the simplest machine, Ubuntu 12.04 x64. The number of processes 2, connections 1024. No additional settings were made. In the event of a server hang, I set a nginx reboot every 10 minutes.