2007-07-14

REST-ified Message Queue on HTTP

REST-ified Message Queue on HTTP[1]

In a post to rest-discuss mailing list, Paul Winkler noted that the REST API offered by activemq is not actually REST-ful at all.

* it's not REST that says GET is a read-only operation; it's HTTP. So
their HTTP implementation is broken. Sadly seems to be pretty common.
* DELETE on a URL representing a queue means you want to delete the
entire queue, not a single message!

He ended with a question "Has anybody thought of a good way to model a queue?". The following is my solution.
A message queue can be thought of as having two ends: one for the producers and the other for the consumers. The producers insert messages into the consumers which will be popped by the consumers. A queue is a simple concept and can be implemented simply in programs. However, once networking capability creeps into the requirements, things can get complicated because network is not reliable. Simple implementations of queue work only because it assumes data transfer within a process is reliable which is a reasonable assumption for in-process activity.
When network is factored in, new problems crop up:

How do you reliably insert messages?

This problem can be paraphrased as doing reliable delivery over HTTP. There are two techniques that address this: Bill de hÓra's HTTPLR and Paul Prescod's.
Using any of these two technique, a producer can POST a message to the same queue URL shared by other producers.
==> POST /queue
    some message
<== 201 Created
    Location: /message/433

How do you reliably allow multiple consumers to pop messages?

This problem expands on the previous one. There is a pop action that has to be considered. In a queue, the pop operation removes and return the head of the queue to the caller. With ActiveMQ, you'd send a DELETE to the queue URL. As Paul Winkler noted, that means the DELETE is applied to the queue, not to individual message.
If one can get a list of messages, then one can repeatedly attempt to pop (by doing DELETE) on each messages. That is, one repeatedly poll for an available message.
==> GET /queue/messages
<== 200 OK
    [list of messages]

// oops, some other client has taken msg #100
==> DELETE /message/100
<== 410 Gone 

==> DELETE /message/102
<== 200 OK
    [message 102]

The polling technique is a form of busy waiting. It is an efficient technique if there is a high probability that the conditions will be met within a short period of time relative to the frequency of polling. To reduce collisions (the 410 Gone in the example above), consumers can try to DELETE randomly.

However, with an increase in number of clients, the frequency of collision increases and efficiency decreases. Furthermore, network is not reliable and client/server can die at any point. When a client issues a DELETE, and it does not receive any response back from the server, it cannot be certain that a message has not been popped for it.

Desirable Properties

So, what are the desirable properties a solution must have?

Client should not Care About Other Client's Existence

The reason polling is done by clients in the example above is because a client has to care about other clients. If it knows for certain that it is the only client, it does not have to poll.
We can put each client in its own virtual world where it can think that it is the only client in existence so it does not have to poll. This is accomplished by giving each client its own access point. Let's implement it by giving each client its own personalised de-queue process resource identified by a unique (per client) URL.

Forgiving to Unreliable Network, Client and Server

In real world, things break unexpectedly2, be it some network admin unplugging a port incidentally, or the server runs out of disk and errors out temporarily, or the client was killed by a OOM condition.
Any implementation of a reliable message queue must be able to at least recover from the above breakages. We can simplify the possible conditions to two cases:
  • Server does not receive the request
  • Client does not receive the response
as the other cases depends on how the client and server are implemented, which I am saving for future topic.

A Solution

The only thing required is for the consumer client to have its own consumer ID. This is not an unreasonable requirement and can be implemented easily (GUID or other simpler mechanisms).
A de-queue is implemented as a sequence of one or more GET requests followed with a DELETE. A GET causes the server to returns a previously bound message to the consumer, if there is any. If there isn't any, return the next unbound message. A DELETE on a message removes the binding on that message.
==> GET /queue/pending_message?consumer_id=12345
<== 200, MSG #32

// repeated GET should yield the same message
==> GET /queue/pending_message?consumer_id=12345
<== 200, MSG #32


// A way to access a particular message
==> GET /queue/message/32
<== 200, MSG #32

// Unbind message 32
==> DELETE /queue/message/32
<== 204

// Since no message is bound to the customer, this GET binds one
GET /queue/pending_message?consumer_id=12345
<== 200, MSG #34

DELETE /queue/message/34
<== 204

// A personal access point allows personalised retrieval criteria
POST /queue/pending_message/filter?consumer_id=12345
<== Return oldest message first
<== 204

// Now retrieves from the oldest to the newest
GET /queue/pending_message?consumer_id=12345
<== 200, MSG #1



[1] I explicitly mentioned 'on HTTP' because there are groups who seem to tied REST and HTTP too strongly for my liking; some even goes as far as equating REST to HTTP.
[2] Does expecting things to break unexpectedly make such breakage expected?

2007-05-17

Surrogate key does not make normalisation

I am not a surrogate-key lover but I understand that there are people who are and situations where it is advantageous.

However, it is not a magic dust and sprinkling it liberally over a database design does not automatically normalise the design. Such myth, unfortunately, has not gone away and seems to be spreading wider. It is as if for every person enlightened, three more replaces him/her.

I think the myth is propagated by the popularity of ORM and difficulties of many ORMs in dealing with relation having non-surrogate keys. But that's a topic for some other time.

Take, for example, the following design:
 
Surrogate Key
Food
Food IdFood
1Apple
2Orange
3Yam
4Carrot
Kind
Kind IdKind
1Fruit
2Root
Food Kind
Food IdKind Id
11
21
32
42


Would you believe that it is equally as normalised as the following:
Natural Key
Food
Food Id
Apple
Orange
Yam
Carrot
Kind
Kind Id
Fruit
Root
Food Kind
Food IdKind Id
AppleFruit
OrangeFruit
YamRoot
CarrotRoot

and vice-versa.

The natural key version is not any more de-normalised nor redundant than the surrogate key version. There is no more redundancy in any one version than the other. One may be tempted to point at the Food Kind relation of the Natural Key version and shout "AHA! There are two 'Fruit' and two 'Root' entries under the Kind Id column. That's redundant." I'd shout back "'AHA!' There are two '1' and two '2' entries at the same place in the Surrogate Key version.

They both are keys. They uniquely identify tuples. If you are referring to the same tuple multiple times, there will be that many copy of the key be it represented with integers or strings.

This article is not debating which key is 'better'. There are many articles addressing the pros and cons of each key type. Simply google for "natural surrogate key".

There are other kind of having multiple copies of the same values. It is known as denormalised state.
De-Normalisation and redundancy goes hand-in-hand. A relation (table in SQL-speak) contains redundant information if the entries contain derivable information. The goal of normalisation is to perform loss-less decomposition such that there is no more derivable information in any relation.
 
De-Normalised Design
Food
Food IdIs Crunchy
Appletrue
Orangefalse
Yamfalse
Carrottrue
Kind
Kind Id
Fruit
Root
Food Kind
Food IdKind IdIs Crunchy
AppleFruittrue
OrangeFruitfalse
YamRootfalse
CarrotRoottrue


The above is de-normalised design because the Is Crunchy field in Food Kind relation is derivable from Food itself.
Normalised Design
Food
Food IdIs Crunchy
Appletrue
Orangefalse
Yamfalse
Carrottrue
Kind
Kind Id
Fruit
Root
Food Kind
Food IdKind Id
AppleFruit
OrangeFruit
YamRoot
CarrotRoot

2007-05-11

Hypermedia, the forgotten aspect of REST

In a post to rest-discuss mailing list, Roy Fielding quizzed if anyone else know what was missing from the REST presentation slides made by Steve Bjorg.
Without looking at the slides and the following posts, can you guess what was missing?
If someone is going to give a presentation on REST, it is likely that they will emphasise the more popular aspects of REST: resource, resource identification, representation of the resource and the uniform methods for interacting and manipulating the resource.
Many presentations that I have seen would further concentrate on the look-and-feel of "REST-ful" URLs, giving the audiences the wrong message that REST is primarily about URL construction. Josh Sled brought a good example of how even a big-name (I'm not telling who, go figure it out from the look-and-feel of the URL) development group got REST wrong: http://developer.yahoo.com/photos/V3.0/createAlbum.html. Whatever do they mean by "Passed in as a REST parameter.", I wonder. Quick, the server ate too many REST parameters, pass in the non-REST ones.
What is missing is hypermedia. It's the last constraint mentioned in the dissertation. http://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm section 5.1.5:
REST is defined by four interface constraints: identification of resources; manipulation of resources through representations; self-descriptive messages; and, hypermedia as the engine of application state.
But whatever does "hypermedia as the engine of application state" mean? I like Mark Baker's explanation:
Hypermedia as the engine of application state" is simply a long-winded way of saying that REST clients make progress via links embedded in the data they retrieve. That sounds like an almost information-free statement, but it's not difficult to violate this constraint, and it can be quite costly to do so as well.
For example, if you have a blog, and there is an entry for 2007-05-11, from your home page, there should be a link pointing to that entry. User and client should not be expected to guess how to get there.
Just as state is explicitly represented and transferred, so must the transitions.

2007-03-28

mdadm Cares About Partition Type

mdadm now starts caring for the partition type when creating a raid volume on partitions. It has to be 'fd' (Linux raid auto).
 
[used fdisk to create sda1, sda2, sdb1, sdb2. 
sda1 and sdb2's type is 82 (linux swap).
sda2 and sdb2's type is 83 (linux)]
 
 
~ $ sudo mdadm --create  /dev/md3 -n 2 -l raid1 --name='raid1 volume set' /dev/sda2 /dev/sdb2

mdadm: /dev/sda2 is too small: 0K
mdadm: create aborted



~ $ sudo mdadm --version

mdadm - v2.5.6 - 9 November 2006


[used fdisk to change sda2 and sdb2 to fd.
"Changed system type of partition 2 to fd (Linux raid autodetect)"]


~ $ sudo mdadm --create  /dev/md3 -n 2 -l raid1 --name='raid1 volume set' /dev/sda2 /dev/sdb

mdadm: array /dev/md3 started. 
 
 
 
 
 
 
(originally from http://microjet.ath.cx/WebWiki/2007.03.28_mdadm_Cares_About_Partition_Type.html)

2007-03-15

1994 nissan sentra fuel injector wierdness

We have two 1994 Nissan Sentra here, Put-Put and Pit-Pit. Put is 160K and Pit is 200K. They both still run strong but that is thanks to having 2 fuel injectors replaced.

It started last November when Pit's cylinder #1 fuel injector went bad. It was so unexpected because fuel injector rarely goes bad. I chalked it up to bad fuel filter. So I replaced the fuel injector and filter.

Then two weeks later, Put's cylinder #1 fuel injector also went bad. That was so surprising. What was the chance of two Sentras having bad fuel injector within weeks of each other. I can't blame it on gasoline because the other car, a 1991 Mazda Protege, was not experiencing any problem, and googling for the problem did not indicate that there was a widespread problem with Sentra's fuel injector. I replaced the fuel injector and filter.

The next day, Put's cylinder #3 fuel injector went bad. In less than 24 hours! I replaced it, and there is no more problem so far.

But last week, Pit's cylinder #4 fuel injector went dead again. By this time, I was so good at replacing Sentra's fuel injector, I did the whole thing from start to finish in under 30 minutes. For comparison purpose, the first time I did it I took 6 hours splitted across 2 days.



2006-11-17

DebianAMD64 and Samsung ML1740 (or othere printers)

Today, M. Aurélien Croc, the author of Splix, informed me that a new version of Splix, 0.0.2, is now usable in 64-bit.

Of course I am happy because now I am no longer limited to submitting 4294967296 documents to the spooler; now I can submit 18446744073709551616 documents at a time. I mean why else would you even bother running a spooler in a 64-bit environment? (I kid).

Packages needed:

libcupsys2-dev
libcupsimage2-dev



/tmp/splix-0.0.2 $ make
make[1]: Entering directory `/tmp/splix-0.0.2/src'
g++ -O2 `cups-config --cflags` -I../include -c -o rastertospl2.o rastertospl2.cpp
g++ -O2 `cups-config --cflags` -I../include -c -o raster.o raster.cpp
g++ `cups-config --ldflags` -lcups -lcupsimage -o rastertospl2 spl2.o printer.o band.o compress.o rastertospl2.o raster.o
make[1]: Leaving directory `/tmp/splix-0.0.2/src'
make[1]: Entering directory `/tmp/splix-0.0.2/ppd'
make[1]: Nothing to be done for `all'.
make[1]: Leaving directory `/tmp/splix-0.0.2/ppd'


/tmp/splix-0.0.2 $ sudo make install
make[1]: Entering directory `/tmp/splix-0.0.2/src'
install -m 755 -s rastertospl2 `cups-config --serverbin`/filter
make[1]: Leaving directory `/tmp/splix-0.0.2/src'
make[1]: Entering directory `/tmp/splix-0.0.2/ppd'
install -d -m 755 `cups-config --datadir`/model/samsung
for filename in ml1510 ml1520 ml1610 ml1710 ml1740 ml1750 ml2010 ml2150 ml2250 ml2550; do \
install -m 644 $filename.ppd `cups-config --datadir`/model/samsung;\
for lang in fr it de; do \
install -m 644 $filename$lang.ppd `cups-config --datadir`/model/samsung;\
done; \
done \

make[1]: Leaving directory `/tmp/splix-0.0.2/ppd'

--- Everything is done! Have fun ---





The files installed are:


/usr/lib/cups/filter/rastertospl2
/usr/share/cups/model/samsung/*.ppd (among them is ml1740.ppd)


We can add the printer from cups's web interface (typically found at http://localhost:631). When prompted for printer model, just provide the ppd (in my case it is /usr/share/cups/model/samsung/ml1740.ppd).


(originally from http://microjet.ath.cx/WebWiki/2006.11.17_DebianAMD64_and_SamsungML1740_Part2.html)

2006-11-05

Continuation support in ruby

I heard today that Ruby 2.0 is going to abandon support for continuation, at least initially until things have stabilised a bit. It is no longer a news, it is just that I have not been active in Ruby community for a while.

The Problem with Continuation Support in Ruby 1.x


My initial reaction upon hearing that was "What a pity". On second thought, I realised that dropping continuation initially makes sense. The support for continuation in Ruby 1.x is missing an ensure procedure that is continuation-friendly; meaning one that is called only once one a given path is no longer accessible.

$create_cont_k=nil

def do_something(k)
  puts "Do something called"
  $create_cont_k = k
end

def do_something_else
  return unless $create_cont_k
  puts "Do something else called"
  k = $create_cont_k
  $create_cont_k = nil
  k.call
end


def create_cont
  callcc{|create_cont_k| do_something(create_cont_k)}
ensure
  puts "Ensure called"
end

create_cont
do_something_else

# /tmp $ ruby1.8  /tmp/ensure-run-multiply.rb 
# Do something called
# Ensure called
# Do something else called
# Ensure called


A proper ensure mechanism that supports explicit continuation formation system, like Ruby 1.8, would have produced:

# /tmp $ ruby1.8  /tmp/ensure-run-multiply.rb 
# Do something called
# Do something else called
# Ensure called


Notice how "ensure" is supposed to be called only once. I have a sample implementation of such continuation-observant ensure mechanism in system-managed-unwind-protect-in-sisc for SISC, a scheme implementation.

"But, but, what if I really want something like ensure that is called each time a code section is called?", you asked. In other words, you want something like Ruby 1.8's multiple-shot ensure in this new world of single-shot ensure.

That is not a problem and don't think that you are being unreasonable for asking this feature. It is useful to be able to guarantee execution of some code upon exiting a dynamic environment.

A dynamic environment is the environment that your program can access at any given time. The environment can change with each instruction executed by the machine.

But such one-sided guarantee does not really do anything interesting. How about a guarantee that some code is also executed upon entering a dynamic environment, thus having a symmetric aspect.

Scheme has dynamic-wind that does the above:

(define *CREATE-CONT-K* #f)

(define (do-something k)
  (display "Do something called\n")
  (set! *CREATE-CONT-K* k))

(define (do-something-else)
  (when *CREATE-CONT-K*
    (display "Do something else called\n")
    (let ((k *CREATE-CONT-K*))
      (set! *CREATE-CONT-K* #f)
      (k))))

(define (create-cont)
  (dynamic-wind
      (lambda () (display "Entering...\n"))
      (lambda ()
        (call/cc
         (lambda (create-cont-k)
           (do-something create-cont-k))))
      (lambda () (display "Exiting...\n"))))

(create-cont)
(do-something-else)

;; /tmp $ sisc -x /tmp/dynwind.scm 
;; Entering...
;; Do something called
;; Exiting...
;; Do something else called
;; Entering...
;; Exiting...

The utility in having a in/excursion guard can't be understated. In SRFI-34, dynamic-wind is used to install a custom exception handler. Yes, a custom exception handler; this is probably a strange concept for some people. Why would one have different exception handlers on the same piece of code? The first reason is "why not?". A dynamic language restricted to having a static exception handler feels incomplete. The second reason is more practical. Since you can in/ex-curse from/to any context (via calling a provided continuation object), how an exception is handled in a given context is not necessarily the same as in other different contexts.

Thus, a complete example of how multiple-shot and single-shot ensure example co-existing (sorry, in scheme as there is no Ruby implementation with single-shot ensure yet):

(class-path-extension-append! (list "file:/home/ysantoso/share/project/scheme/unwind-protect/"))

(require-library 'unwind-protect)
(import unwind-protect)

(import s2j)
(define (do-gc) ((generic-java-method '|gc|) (java-null (java-class '|java.lang.System|))))

;; do-something and do-something-else functions are elided for brevity

(define (create-cont)
  (dynamic-wind
      (lambda () (display "Entering...\n"))
      (lambda ()
        (unwind-protect
          (call/cc
            (lambda (create-cont-k)
              (do-something create-cont-k)))
         (display "Ensure\n")))
      (lambda () (display "Exiting...\n"))))


(create-cont)
(do-gc)
(do-something-else)
(do-gc)

;; /tmp $ sisc -x /tmp/dynwind.scm 
;; Entering...
;; Do something called
;; Exiting...
;; Do something else called
;; Entering...
;; Exiting...
;; Ensure

Notice how "ensure" is called just once even though there are two incursion into the protected code block.

The last deficiency in Ruby 1.x is the restriction that a continuation has to be resumed from the same thread that reifies it.

def do_something_else
  return unless $create_cont_k
  puts "Do something else called"
  k = $create_cont_k
  $create_cont_k = nil
  t=Thread.new { k.call}
  t.join
end

# /tmp $ ruby1.8  ensure-run-multiply.rb 
# Do something called
# Ensure called
# Do something else called
# ensure-run-multiply.rb:13:in `call': continuation called across threads (RuntimeError)
#       from ensure-run-multiply.rb:14:in `join'
#       from ensure-run-multiply.rb:14:in `do_something_else'
#       from ensure-run-multiply.rb:25


(originally from http://microjet.ath.cx/WebWiki/2006.11.05_Continuation_Support_in_Ruby.html)

2006-10-24

System-managed UNWIND-PROTECT in SISC

FORM: (unwind-protect BODYFORM UNWINDFORMS...)
Scheme's call/cc provides a flexible way to control execution flow. Its flexibility allows for different approaches to implementing unwind-protect.

Dorai Sitaram's "Unwind-Protect in Portable Scheme" shows a portable implementation of unwind-protect. It does so by constricting the flexibility of call/cc. User would have to specify when to call the unwindforms part of unwind-protect. In certain situations, it is desirable to have this level of control. In others, it would be akin to having to manually manage memory allocation.

The other approach is having an automatically executed unwindforms such as the one described by Taylor R. Campbell, "2006-05-01 On control brackets and resource release". Having the system manages the execution of unwindforms is more in line to traditional implementations of unwind-protect in other languages. It is this approach that interests me.

By associating a protector cell to the bodyform, we can rely on the GC mechanism cleaning up the protector cell once =bodyform='s continuation is no longer reachable. If it is no longer reachable, then it is safe to execute the unwindforms.

He also describes an optimisation of a common case where =bodyform='s continuation is never reified. Without it, there is only one =bodyform='s continuation and once the execution passes through the end of bodyform, it is safe to continue to unwindforms.

In his 'blog', he provides a sample implementation for MIT-Scheme. The implementation would have to be platform-specific since it hooks into the GC. However, it is easy to port that to other platform.

 

An Implementation in SISC

As a user of SISC, and as someone who has seen many applications (including SISCWeb) using dynamic-wind as a poor-man's unwind-protect, having a system-managed unwind-protect in SISC is a good idea. As I was not aware of any existing implementation, I proceeded to create one using Taylor Campbell's example as the starting point.

To hook into Java's GC finalisation process, I used GCHook.java. Then unwind-protect.scm implements the rest.

unwind-protect-in-sisc


(originally from http://microjet.ath.cx/WebWiki/2006.10.24_UnwindProtectInSISC.html and http://microjet.ath.cx/WebWiki/2006.10.27_UnwindProtectInSISCPart2.html)

2006-09-04

Debian Sarge (3.1), LVM2, and Linux >= 2.6.16

I upgraded Linux kernel from 2.6.15 to 2.6.17 and found out that the weekly backup process appeared to hung the whole machine. I narrowed the problem to when the snapshot volume was being removed:

lvremove /path/to/snapshot/volume

I found this email that describes a workaround, but the more proper workaround is to upgrade sarge's lvm2 package made available by Juergen Kreileder.

(originally from http://microjet.ath.cx/WebWiki/2006.09.04_DebianSarge_LVM2_Linux2.6.16.html)

2006-08-07

perl-like lock for ruby

cdfh was asking in #ruby-lang about a locking library that allows one to do:
 
lock(an_obj) { ..... }

similar to the lock library in perl.

"That won't not that hard to build", I thought. I did the first version in about 5 minutes. Then thought that it would be nice to make it robust to object_id wrap-around and also to discard unnecessary records associate to an_obj when an_obj has been garbage collected.
 
require 'thread'
require 'weakref'


class Lock
  Record = Struct.new("Record", :weakref, :mutex)

  def initialize(auto_cleanup_per_record = 1000)
    @mutex = Mutex.new
    @obj_records = {}
    @create_count = 0
    @auto_cleanup_per_record = auto_cleanup_per_record
  end

  def lock(what, &block)
    record = acquire_obj_record(what)
    record.mutex.synchronize {
      if block_given?
        block.call
      end
    }
  end
  

  private

  def cleanup_proc
    lambda {|id|
      @mutex.synchronize {
        to_delete = []
        @obj_records.each_pair{|k,v|
          if not v.weakref.weakref_alive?
            to_delete << k
          end
        }
        to_delete.each{|id|
          @obj_records.delete(id)
        }
        #puts "Cleanup id: #{to_delete.join(",")}. Size: #{@obj_records.size}: #{@obj_records.keys.join(",")}"
      }
    }
  end

  def acquire_obj_record(what)
    @mutex.synchronize {
      if record = @obj_records[what.object_id] and record.weakref.weakref_alive? 
        record
      else
        ObjectSpace.define_finalizer(what, cleanup_proc)
        @obj_records[what.object_id] = Record.new(WeakRef.new(what), Mutex.new)
      end
    }
  end

  public
  
  @instance = Lock.new
  def self.lock(what, &block)
    @instance.lock(what, &block)
  end
  def self.cleanup(verbose=false)
    @instance.cleanup(verbose)
  end
end



if $0 == __FILE__
  module Test
    def self.test1(finalizer_block)
      puts "Test1"
      s="str"*1024*1024
      ObjectSpace.define_finalizer(s, finalizer_block)
      puts s.object_id
      
      t1 = Thread.new {
        Lock.lock(s) {
          4.times {
            puts "Thread1"
            sleep(0.25)
          }
        }
      }
      
      t2 = Thread.new {
        puts "Thread2 started"
        Lock.lock(s) {
          4.times { 

            puts "Thread2"
            sleep(0.25)
          }
        }
      }
      
      t1.join
      t2.join
    end

    def self.test2(finalizer_block)
      puts "Test2"
      s="noo"*1024*1024
      puts s.object_id
      ObjectSpace.define_finalizer(s, finalizer_block)
      Lock.lock(s)
    end

    def self.test3(finalizer_block)
      puts "Test3"
      10.times {
        s="noo"*1024*1024
        puts s.object_id
        ObjectSpace.define_finalizer(s, finalizer_block)
        Lock.lock(s)
      }
    end
  end



  Test.test1(lambda {|id| puts "Finalized: #{id}"})
  GC.start
  puts "GC invoked"

  Test.test2(lambda {|id| puts "Finalized: #{id}"})
  GC.start
  puts "GC invoked"

  Test.test3(lambda {|id| puts "Finalized: #{id}"})
  GC.start
  puts "GC invoked"
end


=begin
/mnt/vg0.home/ysantoso/tmp/ruby-postgres/tests $ ruby /tmp/lock.rb 
Test1
-604990708
Thread1
Thread2 started
Thread1
Thread1
Thread1
Thread2
Thread2
Thread2
Thread2
GC invoked
Test2
-610787788
GC invoked
Test3
-610788148
-610953348
-615730956
-615809476
-615887996
Finalized: -615730956
Finalized: -610953348
-615902566
-615981086
-615697446
Finalized: -615809476
Finalized: -615887996
Finalized: -615902566
-614121002
-614199522
Finalized: -614121002
Cleanup id: -615809476,-615730956,-610953348,-615902566,-615887996. Size: 7: -614199522,-615697446,-615981086,-610787788,-614121002,-610788148,-604990708
Finalized: -614199522
Cleanup id: -614121002. Size: 6: -614199522,-615697446,-615981086,-610787788,-610788148,-604990708
Finalized: -615697446
Cleanup id: -614199522. Size: 5: -615697446,-615981086,-610787788,-610788148,-604990708
Finalized: -615981086
Cleanup id: -615697446. Size: 4: -615981086,-610787788,-610788148,-604990708
Finalized: -610787788
Cleanup id: -615981086. Size: 3: -610787788,-610788148,-604990708
Finalized: -610788148
Cleanup id: -610787788. Size: 2: -610788148,-604990708
GC invoked
Finalized: -604990708

=end 
 
 
(originally from http://microjet.ath.cx/WebWiki/2006.08.07_LockLibraryForRuby.html)