Transaction semantics

8 views
Skip to first unread message

Patru

unread,
Dec 25, 2011, 10:28:57 AM12/25/11
to MagLev Discussion
Last night I tried to coax one of my classes into some sort of rails
semantics, so it should have a save method (saving it to a persistent
class internal Hash of instances) which would assign it with a unique
(short) id when saved for the first time. Save should commit directly
if there was no "greater" transaction in progress. First problem:
getting sequence-like semantics from Maglev, I ended up doing the
following:

def self.next_number
begin
Maglev.abort_transaction if Maglev.transaction_level == 1
@@sequence = @@sequence+1
Maglev.commit_transaction if Maglev.transaction_level == 1
@@sequence
rescue Maglev::CommitFailedException
redo
end
end

in a class method. This made the save method pretty simple as well:

def save
Maglev.abort_transaction if Maglev.transaction_level == 1
if id.nil?
@id=self.class.next_number
end
self.class.instances[id]=self
Maglev.commit_transaction if Maglev.transaction_level == 1
end

and it works pretty well, committing automagically on the top level
and leaving the transaction semantics of a nested transaction to the
programmer if he chooses to Maglev.begin_nested_transaction.

However, the the '... if Maglev.transaction_level == 1' all around
leaves a bad code smell. The resulting code is fairly practical and
"Rails-like" in a way, but I would like to know if there was a better
way to achieve a similar result. It also looks generic enough to be
extracted into a module that could be included in some of my models if
all I wanted was a direct access through its id. Of course I also get

def self.find(id)
Maglev.abort_transaction if Maglev.transaction_level == 1
self.instances[id]
end

thrown into the deal which again looks exceedingly simple (and
probably fast as well).

In auto-transaction mode it pretty much minimizes contention. I think
there is no way to reduce contention in nested transactions as they
will always interfere and the programmer will have to deal with
whatever he writes.

Anyways, I would be glad to hear your opinions on this kind of an
approach. Currently it runs my Tests and I hope to write my next
invoices on this basis soon :-)

Tim Felgentreff

unread,
Dec 25, 2011, 12:10:23 PM12/25/11
to maglev-d...@googlegroups.com
There's be a better way to achieve what you're trying to do with the global counters here.
You really want indices to count globally, without regard for whether the object corresponding to the index has already been committed or not. There's a mechanism in Gemstone to achieve something like that, called "Global Counters". From the documentation:

Persistent shared counters provide a means for multiple sessions to share common integer values.  There are 128 persistent shared counters, numbered from 1 to 128 (the index of the first counter is 1).

Each update to a persistent shared counter causes a roundtrip
to the stone process.  However reading the value of a counter is
handled by the gem (and its page server, if any) and does not
cause a roundtrip to the stone.

Persistent shared counters are globally visible to all sessions on all shared page caches. Persistent shared counters hold 64 bit values and may be set to any signed 64 bit integer value.  No limit checks are done when incrementing or decrementing a counter.  Attempts to increment/decrement the counter above/below the minimum/maximum value of a signed
64-bit integer will cause the counter to 'roll over'.

Persistent shared counters are independent of database transactions. Updates to counters are visible immediately and aborts have no effect on them.

This functionality is available in Smalltalk as "System class>>persistentCounterAt:incrementBy:", which corresponds to "Maglev::System#increment_pcounter(counter, by=1)". The methods returns the new value. There's more documentation in maglev/src/kernel/bootstrap/System.rb

Using that mechanism, you're method could shorten to:

 def self.next_number
   # 0 < OBJECT_ID_SLOT < 128
   Maglev::System.increment_pcounter(OBJECT_ID_SLOT)
 end

Re nested transactions. If you plan on using those, maybe it'd be worthwhile to add your own #commit method to iron over the differences between nested and top-level transactions (it might be worth having that in the core, at some point). Could look something like this:

  def keeping_transaction_level(&block)
    was_nested = Maglev.transaction_level > 1
    yield
    Maglev.begin_nested_transaction if was_nested
  end

  def commit
    keeping_transaction_level do
      Maglev.commit_transaction
    end
  end

  def abort
    keeping_transaction_level do
      Maglev.abort_transaction
    end
  end

And then, your methods could look like:

 def save
   abort

   if id.nil?
     @id=self.class.next_number
   end
   self.class.instances[id]=self
   commit
 end

 def self.find(id)
   abort
   self.instances[id]
 end

Note: All this was coded in the Gmail webinterface, so beware typos and logical errors :)

> --
> You received this message because you are subscribed to the Google Groups "MagLev Discussion" group.
> To post to this group, send email to maglev-d...@googlegroups.com.
> To unsubscribe from this group, send email to maglev-discuss...@googlegroups.com.
> For more options, visit this group at http://groups.google.com/group/maglev-discussion?hl=en.
>

Patru

unread,
Dec 25, 2011, 5:44:51 PM12/25/11
to MagLev Discussion
Hi Tim, thanks a lot for the information regarding pcounters as it
seems to be hard to find at the moment. Until now not even Google
would know about them in the context of Maglev, that will probably
change upon the next index of this group which is a good thing. There
are 4 methods concerning pcounters in Maglev::System,

#decrement_pcounter, #increment_pcounter, #pcounter and #set_pcounter

with pretty obvious semantics. I was originally planning to use one
counter per entity (as you would do with sequences in Oracle or other
databases) but given that there are only 128 of those counters I will
probably only use one of them for the whole application. Ultimately
URLs will be a bit longer, but that should not be a big issue.

I am pretty sure pcounters are the better solution than my clumsy
approximation, but it at least satisfied my tests :-)

Concerning nested transactions your code seems to achieve the same
thing I intended without any of the "smell", thanks for the pointer.
In fact I wanted to be able to persistently save my object with
minimal fuss and still be able to rollback my MiniTest::Specs in a
nested transaction. This seems to be achieved perfectly.

I guess I will put my code up on github as an open source example for
Maglev as soon as it can actually produce invoices. There seem to be
fairly little examples at the moment.

On 25 Dez., 18:10, Tim Felgentreff <timfelgentr...@gmail.com> wrote:
> There's be a better way to achieve what you're trying to do with the global
> counters here.
>
...
> This functionality is available in Smalltalk as "System
> class>>persistentCounterAt:incrementBy:", which corresponds to
> "Maglev::System#increment_pcounter(counter, by=1)". The methods returns the
> new value. There's more documentation in
> maglev/src/kernel/bootstrap/System.rb
>
> Using that mechanism, you're method could shorten to:
>
>  def self.next_number
>    # 0 < OBJECT_ID_SLOT < 128
>    Maglev::System.increment_pcounter(OBJECT_ID_SLOT)
>  end
...

Patru

unread,
Dec 25, 2011, 6:55:27 PM12/25/11
to MagLev Discussion
As far as I can tell pcounters worked as intended, however, there is
one more catch with the transaction logics We may not abort the
current transaction if we are in a nested transaction, even if we keep
the same transaction level, as this will throw away the work that has
been done in the transaction so far (it will not just sync the current
state, but throw away whatever we did. It would almost run my
(obviously imperfect) test suite, but failed to find records after
they had been committed in a nested transaction (as the find-command
would abort one more level before trying to access any more). This
does not invalidate the Tims approach, but I will have to think about
how to reformulate keep_transaction_level in order to avoid this
problem.

Patru

unread,
Dec 25, 2011, 7:14:59 PM12/25/11
to MagLev Discussion
In fact the following addition to module Maglev proved successful:

module Maglev
def self.keeping_transaction_level(&block)
yield unless Maglev.transaction_level > 1
end

def self.commit_level
keeping_transaction_level do
Maglev.commit_transaction
end
end

def self.abort_level
keeping_transaction_level do
Maglev.abort_transaction
end
end
end

However this has slightly semantics. Instead of aborting/committing
transactions regardless of the level and readjusting the level
afterwards (as Tims code did) it does *only* abort/commit when we are
on level 1 (as my code did before), leaving the responsibility of
committing/aborting any nested transactions to whoever initiated them.
This looks sound for the moment, as it provides the ability to commit
while on level one an leaving alone any nested transactions that have
been initiated beforehand.

As an alternative we might opt to initiate another nesting level if
there already is one in order to get a Maglev::CommitFailedException
as quickly as possible. However without much knowledge about the
nested transaction implementation it is hard to tell wether this would
fail any faster.
Reply all
Reply to author
Forward
0 new messages