-
Notifications
You must be signed in to change notification settings - Fork 80
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposed SysV structures and Mixin functionality #64
Conversation
6a888c3
to
bcb187d
Compare
@@ -3,7 +3,7 @@ module Simple | |||
class Integer #:nodoc: | |||
attr_accessor :value | |||
|
|||
def initialize | |||
def initialize(**_) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't need the _
here. If you disregard everything.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So just def initialize(**)
? I didn't know that.
Fine by me. As said though
Synchronize is definitely much better.
Nah, an accessor is always good. The rest of the PR is out of my my expertise. |
@csfrancis Any suggestions or comments on the C side? A working version can be found here in the old branch which I keep updated, but probably reading the long PR description up there ^ would be enough. |
end | ||
|
||
def self.included(base) | ||
def base.do_with_sync(*names) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@byroot probably the most interesting change that you could provide an opinion for is this function here. I call it from C to replace a regular accessor or setter that accesses shared memory and is not a atomic call, and wrap it in a synchronize
. I wrap all the functions with this, so it's essentially a do_with_sync :value=, :value, :reset, :increment
and do_with_sync :size=, :max_size, :<<, :push, :pop
etc
I just want to make sure there are no disagreements here.
Relevant C code is
https://github.com/Shopify/semian/blob/circuit-breaker-per-host/ext/semian/semian_shared_memory_object.c#L319-L324
where define_method_with_synchronize
calls do_with_sync
and
https://github.com/Shopify/semian/pull/54/files#diff-99c3116b9c2c62cc89a4ae4cc84fadaaR262
which is where define_method_with_synchronize
is used in place of rb_define_method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer to avoid this kind of monkey patching.
I'd rather prefer have a noop sychronize
method in Simple
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While looking over the C code base, one of the scarier things was that because I provided the lock()
and unlock()
as separate functions, it was very difficult to track where I had to re-unlock, since errors could occur in the middle of a function, and then you'd have to return
right there, and insert an unlock()
there. It just isn't safe to have many exit points sprawled all over the code. So I then opted to write ensures
in, but doing so manually would add twice the number of functions and make things very manual.
Here's the what a begin
-ensure
would look like:
return rb_ensure(semian_shm_object_synchronize_with_block, self, semian_shm_object_synchronize_restore_lock_status, (VALUE)&status);
For reference: https://github.com/Shopify/semian/pull/54/files#diff-905e62ddb202d77b001ca1dbac3cbf06R310
Essentially each function needed a wrapper, and probably also some custom struct to overcome the one-argument limit of the function rb_ensure
. This naturally lead to me doing it in Ruby instead since it doesn't have these limitations, which is why synchronize
does what it does. IMO it's not a big deal since it only does it once per class, reduces code and complexity by a bit. Maybe using prepend
with its use of super
would make it cleaner than doing what I do currently, which is having an #{method_name}_inner
as the original and ##{method_name}
as the synchronized version.
With regards to the noop synchronize
, do you mean something more along the lines of (1) , with SharedMemoryObject
-> Simple::Integer
-> SysV::Integer
? Otherwise I don't understand what you mean by noop synchronize
. Unless you mean just having one there to keep the interfaces the same?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kyewei I think what he means with noop syncronize is that the SysV implementations continue to inherit from Simple::Integer
and friends, which then look like this:
class Semian::Simple::Integer
# .. stuff
def increment(delta)
synchronize { value += delta }
end
private
def synchronize
yield
end
# more stuff
end
When this mixin is included, it just overrides it. The implementation can then be simplified when all the fallback checking is done at boot-time because you can just override it in C, and not have it in Ruby.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see what you're saying, but that also doesn't work in protecting lets say.. Semian::SysV::Integer#value
. If the #value
is not protected in C code, and it directly replaces the SysV::Integer
, there is no entry point to override and replace with synchronize { ... }
except through what do_with_sync
does. If you do protect in C, well, that goes back to the problem of making 2x the functions to call rb_ensure(callback, arg1, ensure_fn, arg2)
For example, in code,
def increment(val=1)
synchronize { self.value += 1 } # OK
end
def value
#how do you put a synchronize in here?
# you can't do self.value, that would just do recursion and crash
@value
end
@@ -159,6 +160,10 @@ def resources | |||
require 'semian/simple_sliding_window' | |||
require 'semian/simple_integer' | |||
require 'semian/simple_state' | |||
require 'semian/sysv_shared_memory' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should these require
s be conditional depending on whether the target platform supports SysV?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree.
@sirupsen wanted to see the tests, so I'll include them now. Some of those new tests will fail because the C component isn't included in this PR :( They do succeed on the other branch however. |
@@ -1,3 +1,5 @@ | |||
require 'forwardable' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you need this now?
I think that 2 is the right approach as well. |
module SysVSharedMemory #:nodoc: | ||
@type_size = {} | ||
def self.sizeof(type) | ||
size = (@type_size[type.to_sym] ||= (respond_to?(:_sizeof) ? _sizeof(type.to_sym) : 0)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not just use @type_size[type.to_sym]
and make this easier to read? I assume the only reason you need the respond_to?
is because you don't have the C implementation just yet?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's for memoizing the sizes after it's read once from C. I don't want to hardcode sizes, they differ by platform.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with that, but I don't see why you need the local variable when you have the class instance variable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh. For raising the TypeError. Maybe that should be moved to C instead?
def acquire_memory_object(name, data_layout, permissions) | ||
return @using_shared_memory = false unless Semian.semaphores_enabled? && respond_to?(:_acquire) | ||
|
||
byte_size = data_layout.inject(0) { |sum, type| sum + ::Semian::SysVSharedMemory.sizeof(type) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does sizeof
need to be a class method?
Fundamentally I think what makes this PR more complicated than it needs to be is that the fallback is done within the SysV driver, instead of at boot time as @csfrancis also pointed out. This will simplify I bunch of things because you can make more assumptions. |
require 'test_helper' | ||
|
||
class TestSysVSlidingWindow < MiniTest::Unit::TestCase | ||
CLASS = ::Semian::SysV::SlidingWindow |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a little bit of a nitpick, but I'd call this KLASS
instead of make it a method. CLASS
is too close to the reserved keyword.
c300b81
to
89075a2
Compare
end | ||
|
||
def value | ||
NUM_TO_SYM.fetch(@integer.value) { raise ArgumentError } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you did alias_method :to_i, :value
and always called value.to_i
in Simple::State
, you wouldn't need to override these. Duck typing hard!
Old massive PR: #54
Small PR 1: #62
Possibly outstanding issues
SysV
versions of the structure needing permissions and a name,Semian::Simple::SlidingWindow.new(max_size: 4)
andSemian::SysV::SlidingWindow.new(max_size: 4, permissions: 0660, name: "sample_int")
have different interfaces. Ways to solvedef initialize(max_size: , **)
current choicedef initialize(options = {})
or something of the sortpermissions
andname
at all, but they have to come from somewhere else without leaking the implementation, possible fromSemian.register
TestCircuitBreaker
iscircuit_breaker_test.rb
execute_atomically
: you know what, I changed it to#synchronize
, similar to classes likeMutex
andMonitor
. It's still aliased to#transaction
#shared?
It's private, and just returns@using_shared_memory
. Hopefully won't ruffle any feathers, or maybe just theivar
is enough?name
andpermissions
to the structures was bad. Personally I disagree, but if so, start a discussion below about it.Content
Alright, here's what I propose for adding in the SysV structures. I didn't include the tests yet, they will obviously fail because there's no C code to provide actual shared memory in this commit.
The three
SysV
classes inherit directly from theSimple
equivalents. They also mixin theSemian::SysVSharedMemory
module, which provides the common functionality.From the C perspective, to get the memory allocation and things to work, it needs to do a bunch of things. Certain ways of accomplishing them are IMO worse than others, but I'll list the things that the C side needs to do, and the different ways to do them, and why/how I decided on this way.
C needs to accomplish:
acquire_memory_object
is called. The C code needs this when unpacking the RubyVALUE self
object into a proper Csemian_shm_object *
#acquire_shared_memory
at a shared level#value=
,#<<
, etcThis is about to get heavy.
There were 3 ways of doing the above, and there is a diamond problem here. I originally had (1), but I changed to (2).
(1): Have a superclass
SharedMemoryObject
, andSimple
structures inherit from that. TheSysV
further inherit from theSimple
class Simple::Integer < SharedMemoryObject
even though it doesn't do sharing at all(2): Mixin a
SysVSharedMemory
module that does shares common functionality into theSysV
variants onlyPros: Makes sense from class organization perspective, mixin a module if you want shared-ness
Cons: Although you can hook methods like
acquire_memory_object
in one spot, you have to replace the three different allocation methods separately. There is also a timing issue involved. You can't simply do this:Instead, as I found out, you had to do something like this in the C extension:
(3): Make the
SysV
classes inherit fromSharedMemoryObject
. But what do we do about the common functionality betweenSimple
andSysV
versions? Put that in a module, and include it into things that need the functionality, like this:If you made it this far, 👍, it was really long.
I chose (2) because it's the most structurally clean, and I have a working solution to the timing problem, and there are no random abuses of Ruby features like Mixins beyond what is necessary.
I've isolated the locking/unlocking, and by doing that, eliminated the lock() and unlock() functions for a synchronize(), and using some delegating I wrap the methods with lock() and
ensure
unlock().Here's the typical structure of an
acquire
that encompasses the C features as well just so we're on the same page:Here's the structure of a synchronized access. Any method defined using
define_method_with_synchronize
is synchronized.define_method_with_synchronize
basically defines the original method, and then callsdo_with_sync :method_name
Discussion, opinions, etc would be great. I also want suggestions as to how to go forward with this.
A version with everything working is on the branch with the large PR, #54
@sirupsen @byroot