Skip to content

Commit

Permalink
docs + client.write(nil) + fix parsing of last argument
Browse files Browse the repository at this point in the history
  • Loading branch information
boazsegev committed Dec 2, 2024
1 parent c5928ba commit ac4298b
Show file tree
Hide file tree
Showing 5 changed files with 55 additions and 57 deletions.
80 changes: 34 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Iodine includes native support for:
* Event-Stream Pub/Sub (with optional Redis Pub/Sub scaling);
* Fast(!) builtin Mustache template render engine;
* Static File Service (with automatic `.gz`, `.br` and `.zip` support for pre-compressed assets);
* Performant Request Logging to `stderr`;
* Performant Request Logging;
* Asynchronous Tasks and Timers (memory cached);
* HTTP/1.1 keep-alive and pipeline throttling;
* Separate Memory Allocators for Heap Fragmentation Protection;
Expand Down Expand Up @@ -133,13 +133,13 @@ On Rails:

### Logging

To enable performant logging from the command line, use the `-v` (verbose) option:
To enable performant HTTP request logging from the command line, use the `-v` (verbose) option:

```bash
bundler exec iodine -p $PORT -t 16 -w -2 -www /my/public/folder -v
```

Iodine will cache the date and time String data when answering multiple requests during the same time frame, improving performance.
Iodine will cache the date and time String data when answering multiple requests during the same time frame, improving performance by minimizing system calls.

### Static Files and Assets

Expand All @@ -151,24 +151,13 @@ Since the Ruby layer is unaware of these requests, logging can be performed by t
To use native static file service, setup the public folder's address **before** starting the server.

This can be done when starting the server from the command line:
This can be done when starting the server either using the Ruby API or from the command line:

```bash
bundler exec iodine -t 16 -w 4 -www /my/public/folder
```

Or using a simple Ruby script. i.e. (a `my_server.rb` example):

```ruby
require 'iodine'
# static file service
Iodine.listen, service: :http, public: '/my/public/folder'
# for static file service, we need no worker processes nor worker threads.
# However, it is good practice to use at least one worker for hot restarts
Iodine.threads = 0
Iodine.workers = 1
Iodine.start
```
Iodine will automatically test for missing extension file names, such as `.html`, `.htm`, `.txt`, and `.md`, as well as a missing `index` file name when `path` points to a folder.

#### Pre-Compressed assets / files

Expand All @@ -182,7 +171,7 @@ gzip -k -9 style.css

This results in both files, `style.css` (the original) and `style.css.gz` (the compressed).

When a browser that supports compressed encoding (which is most browsers) requests the file, iodine will recognize that a pre-compressed option exists and will prefer the `gzip` compressed version.
When a browser that supports compressed encoding requests the file (and most browsers do), iodine will recognize that a pre-compressed option exists and will prefer the `gzip` compressed version.

It's as easy as that. No extra code required.
Expand Down Expand Up @@ -335,7 +324,7 @@ Redis Support Limitations:
* Iodine's Redis client does *not* support multiple databases. This is both because [database scoping is ignored by Redis during pub/sub](https://redis.io/topics/pubsub#database-amp-scoping) and because [Redis Cluster doesn't support multiple databases](https://redis.io/topics/cluster-spec). This indicated that multiple database support just isn't worth the extra effort and performance hit.
* The iodine Redis client will use two Redis connections for the whole process cluster (a single publishing connection and a single subscription connection), minimizing the Redis load and network bandwidth.
* The iodine Redis client will use two Redis connections for each process cluster (a single publishing connection and a single subscription connection), minimizing the Redis load and network bandwidth.
* Connections will be automatically re-established if timeouts or errors occur.
Expand All @@ -345,9 +334,9 @@ Iodine will "hot-restart" the application by shutting down and re-spawning the w
This will clear away any memory fragmentation concerns and other issues that might plague a long running worker process or ruby application.
This could be used for hot-reloading or hot-swapping of the Web Application code itself, but only if the code is lazily in worker processes (never loaded by the root process).
This could be used for hot-reloading or hot-swapping the Web Application code itselfbut only if the code is lazily loaded by each worker processes (never loaded by the root process).
To hot-restart iodine, send the `SIGUSR1` signal to the root process.
To hot-restart iodine, send the `SIGUSR1` signal to the root process or `SIGINT` to a worker process.
The following code will hot-restart iodine every 4 hours when iodine is running in cluster mode:
Expand Down Expand Up @@ -567,7 +556,7 @@ require 'iodine'
# an echo protocol with asynchronous notifications.
class EchoProtocol
# `on_message` is called when data is available.
def on_message client, buffer
def self.on_message client, buffer
# writing will never block and will use a buffer written in C when needed.
client.write buffer
# close will be performed only once all the data in the write buffer
Expand All @@ -586,7 +575,7 @@ end
tls = USE_TLS ? Iodine::TLS.new("localhost") : nil
# listen on port 3000 for the echo protocol.
Iodine.listen(port: "3000", tls: tls) { EchoProtocol.new }
Iodine.listen(service: :raw, tls: tls, handler: EchoProtocol)
Iodine.threads = 1
Iodine.workers = 1
Iodine.start
Expand All @@ -598,54 +587,53 @@ Or a nice plain text chat room (connect using `telnet` or `nc` ):
require 'iodine'

# a chat protocol with asynchronous notifications.
class ChatProtocol
def initialize nickname = "guest"
@nickname = nickname
end
def on_open client
module ChatProtocol
def self.on_open client
puts "Connecting #{client[:nickname]} to Chat"
client.subscribe :chat
client.publish :chat, "#{@nickname} joined chat.\n"
client.timeout = 40
client.publish :chat, "#{client[:nickname]} joined chat.\n"
end
def on_close client
client.publish :chat, "#{@nickname} left chat.\n"
def self.on_close client
client.publish :chat, "#{client[:nickname]} left chat.\n"
puts "Disconnecting #{client[:nickname]}."
end
def on_shutdown client
def self.on_shutdown client
client.write "Server is shutting down... try reconnecting later.\n"
end
def on_message client, buffer
def self.on_message client, buffer
if(buffer[-1] == "\n")
client.publish :chat, "#{@nickname}: #{buffer}"
client.publish :chat, "#{client[:nickname]}: #{buffer}"
else
client.publish :chat, "#{@nickname}: #{buffer}\n"
client.publish :chat, "#{client[:nickname]}: #{buffer}\n"
end
# close will be performed only once all the data in the outgoing buffer
client.close if buffer =~ /^bye[\r\n]/i
end
def ping client
client.write "(ping) Are you there, #{@nickname}...?\n"
def self.on_timeout client
client.write "(ping) Are you there, #{client[:nickname]}...?\n"
end
end

# an initial login protocol
class LoginProtocol
def on_open client
module LoginProtocol
def self.on_open client
puts "Accepting new Client"
client.write "Enter nickname to log in to chat room:\n"
client.timeout = 10
end
def ping client
def self.on_timeout client
client.write "Time's up... goodbye.\n"
client.close
end
def on_message client, buffer
def self.on_message client, buffer
# validate nickname and switch connection callback to ChatProtocol
nickname = buffer.split("\n")[0]
while (nickname && nickname.length() > 0 && (nickname[-1] == '\n' || nickname[-1] == '\r'))
nickname = nickname.slice(0, nickname.length() -1)
end
if(nickname && nickname.length() > 0 && buffer.split("\n").length() == 1)
chat = ChatProtocol.new(nickname)
client.handler = chat
client[:nickname] = nickname
client.handler = ChatProtocol
client.handler.on_open(client)
else
client.write "Nickname error, try again.\n"
on_open client
Expand All @@ -654,9 +642,9 @@ class LoginProtocol
end

# listen on port 3000
Iodine.listen(port: 3000) { LoginProtocol.new }
Iodine.listen(url: 'tcp://0.0.0.0:3000', handler: LoginProtocol, timeout: 40)
Iodine.threads = 1
Iodine.workers = 1
Iodine.workers = 0
Iodine.start
```

Expand Down
7 changes: 5 additions & 2 deletions ext/iodine/iodine.c
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,11 @@ static void Init_Iodine(void) {
iodine_verbosity_set,
1);

rb_define_module_function(iodine_rb_IODINE, "run", iodine_defer_run_async, 0);
rb_define_module_function(iodine_rb_IODINE, "defer", iodine_defer_run, 0);
rb_define_module_function(iodine_rb_IODINE, "run", iodine_defer_run, 0);
rb_define_module_function(iodine_rb_IODINE,
"async",
iodine_defer_run_async,
0);

rb_define_module_function(iodine_rb_IODINE,
"run_after",
Expand Down
10 changes: 7 additions & 3 deletions ext/iodine/iodine_arg_helper.h
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ static int iodine_rb2c_arg(int argc, const VALUE *argv, iodine_rb2c_arg_s *a) {
/* last parameter is special, as it may be a named parameter Hash */
tmp = argv[argc];
i = argc; /* does `for` add even after condition breaks loop? no, so why? */

if (RB_TYPE_P(tmp, RUBY_T_HASH)) { /* named parameters (hash table) */
VALUE tbl = tmp;
if (a[i].expected_type == 0)
Expand All @@ -245,9 +246,12 @@ static int iodine_rb2c_arg(int argc, const VALUE *argv, iodine_rb2c_arg_s *a) {
}
return 0;
}
do {
IODINE_RB2C_STORE_ARG();
} while (0);
if (a[i].rb) {
do {
IODINE_RB2C_STORE_ARG();
} while (0);
++i;
}
tmp = Qnil;
}
for (; a[i].rb; ++i) {
Expand Down
8 changes: 6 additions & 2 deletions ext/iodine/iodine_connection.h
Original file line number Diff line number Diff line change
Expand Up @@ -1919,7 +1919,7 @@ FIO_IFUNC iodine_connection_args_s iodine_connection_parse_args(int argc,
r.hint = r.url_data.scheme;
if (r.hint.len > 8)
rb_raise(rb_eArgError, "service hint too long");

FIO_LOG_DDEBUG("iodine connection hint: %.*s", (int)r.hint.len, r.hint.buf);
if (!r.hint.buf || !r.hint.len ||
FIO_BUF_INFO_IS_EQ(r.hint, FIO_BUF_INFO2((char *)"sses", 3)) ||
FIO_BUF_INFO_IS_EQ(r.hint, FIO_BUF_INFO2((char *)"sses", 4)) ||
Expand Down Expand Up @@ -2268,6 +2268,8 @@ FIO_IFUNC VALUE iodine_connection_write_internal(VALUE self,
to_write = FIO_STR_INFO2(RSTRING_PTR(data), (size_t)RSTRING_LEN(data));
// TODO: use Ruby encoding info for WebSocket?
// fio_http_websocket_write(c->http, to_write.buf, len, is_text)
} else if (data == Qnil) {
to_write = FIO_STR_INFO0;
} else if (rb_respond_to(data, IODINE_FILENO_ID)) {
goto is_file;
} else {
Expand Down Expand Up @@ -2658,7 +2660,9 @@ static void *iodine_tcp_listen(iodine_connection_args_s args) {
STORE.hold((VALUE)args.settings.udata);
*protocol = IODINE_RAW_PROTOCOL;
protocol->timeout = 1000UL * (uint32_t)args.settings.ws_timeout;

FIO_LOG_DEBUG("iodine will listen for raw connections on %.*s",
(int)args.url.len,
args.url.buf);
return fio_io_listen(.url = args.url.buf,
.protocol = protocol,
.udata = args.settings.udata,
Expand Down
7 changes: 3 additions & 4 deletions lib/iodine/documentation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,6 @@ def self.listen(url = "0.0.0.0:3000", handler = nil, service = nil); end
# Code runs in both the parent and the child.
def self.on_state(state, &block); end


# Runs a block of code in the main IO thread (adds the code to the event queue).
#
# Always returns the block of code to executed (Proc object).
Expand All @@ -197,7 +196,7 @@ def self.on_state(state, &block); end
# or Iodine's IO will hang while waiting for the blocking code to finish.
#
# Note: method also accepts a method object if passed as a parameter.
def self.defer(&block); end
def self.run(&block); end

# Runs a block of code in the a worker thread (adds the code to the event queue).
#
Expand All @@ -210,7 +209,7 @@ def self.defer(&block); end
# The code may run in parallel with other calls to `run` or `defer`, as it will run in one of the worker threads.
#
# Note: method also accepts a method object if passed as a parameter.
def self.run(&block); end
def self.async(&block); end

# Runs the required block after the specified number of milliseconds have passed.
#
Expand All @@ -227,7 +226,7 @@ def self.run(&block); end
# | `:block` | (required) a block is required, as otherwise there is nothing to perform. |
#
#
# The event will repeat itself until the number of repetitions had been depleted.
# The event will repeat itself until the number of repetitions had been depleted or until the event returns `false`.
#
# Always returns a copy of the block object.
def self.run_after(milliseconds, repetitions = 1, &block); end
Expand Down

0 comments on commit ac4298b

Please sign in to comment.