Open kbucheli opened 4 years ago
A version without Cro and the need to trigger it from externally:
#!/usr/bin/env raku
use Prometheus::Client :metrics;
use Prometheus::Client::Exposition :render;
sub MAIN() {
my $stopped = Promise.new;
LEAVE { $stopped.keep }
# prepare access to metrics
my $*PROMETHEUS = Prometheus::Client::CollectorRegistry.new;
my $metric-worker-started = counter(
name => 'worker_started',
documentation => 'total number of started worker runs',
label-names => <worker pick1 pick2>
);
my $metric-worker-finished = counter(
name => 'worker_finished',
documentation => 'total number of finished worker runs',
label-names => <worker pick1 pick2>
);
# kick off workers scheduling
my @workers = (^20).map: -> $num {
start {
while !$stopped {
my $pick1 = <a b c d e f g h i j>.pick;
my $pick2 = <q r s t u v w x y z>.pick;
my $labels = \(:worker($num.Str), :$pick1, :$pick2);
$metric-worker-started.labels(|$labels).inc;
sleep rand/10;
$metric-worker-finished.labels(|$labels).inc;
}
}
};
# render the metrics
push @workers, start {
while !$stopped {
render-metrics($*PROMETHEUS);
sleep rand/100;
}
};
# wait for the end of the world
react {
for @workers -> $worker {
whenever $worker {} # forward exceptions
}
# finish up properly with signals
whenever signal(SIGINT,SIGKILL) { done }
}
return;
}
Often it breaks fast, but sometimes not.
Feels to me you're accessing $stopped
from multiple threads at the same time without concurrency control? Perhaps $stopped
doesn't need to be a Promise
but could be an atomicint
. And then use an atomic fetch to see if the flag has changed and an atomic update on to mark it kept?
Promises
are used for concurrency control, no? That part runs since two years in production without issues.
The problems started after adding render-metrics
which creates many intermediate strings and seams to somehow stress out memory management. Could well be that there is a concurrency issue in Prometheus::Client, but this is how it is supposed to be used.
But even if there are concurrency issues in the Raku code it should not be able to corrupt the memory on VM level.
But even if there are concurrency issues in the Raku code it should not be able to corrupt the memory on VM level.
I'm not sure @jnthn agrees.
Interesting, it does atomic operations with Real
, e.g. in the Counter class. Does this work?
Indeed I found a not thread safe part in Prometheus::Client::Metrics::Group
:
method labels(*@label-values, *%labels --> Collector) {
my $labels-key = self!make-labels(@label-values, %labels);
%!metrics{ $labels-key } //= $.factory.build(...);
}
where %!metrics
can be updated by several threads at the same time and the same key.
So I changed it to
has Lock::Async $!label-adding-lock = Lock::Async.new;
method labels(*@label-values, *%labels --> Collector) {
my $labels-key = self!make-labels(@label-values, %labels);
with %!metrics{ $labels-key } {
return $_;
}
$!label-adding-lock.protect: {
return %!metrics{ $labels-key } //= $.factory.build(...);
};
}
and now the memory corruption does not happen any more with my test script. That made me think:
render-metrics
task does not add to the problem?
It does, because when I remove it and use the original version the corruption does not happen.%!metrics
, but my test script is just not as bad to again trigger the corruption.So I come now to the question I wanted to ask to @jnthn at some point when I see him in person: How safe is unprotected concurrent read and write access to arrays and hashes? Safe in the sense of not breaking internals, not in the sense of not creating race conditions or other logical flaws. There are situations where the above hash initialization code is logically safe, e.g. for a cache of constant values. Rarely one value might be calculated twice, but that does not hurt as it is at the end be the same .
How safe is unprotected concurrent read and write access to arrays and hashes? Safe in the sense of not breaking internals, not in the sense of not creating race conditions or other logical flaws.
At the moment, it can break things pretty badly on the MoarVM backend. That's considered a shortcoming that should be addressed; exceptions or data inconsistencies are acceptable, but the potential to bring the VM down as is achievable today is not wanted. I'm aware of some work in that direction; it's a non-small amount of effort, since there's no way to fix the current hash data structure used, and so a completely different approach is needed. Arrays also need a rather tedious reorganization, and we probably have to accept some curiosities in terms of growth edges being a few elements off the powers of 2 or other nice round numbers (though it's unlikely anyone will notice) or accept an extra indirection (not so desirable).
There are situations where the above hash initialization code is logically safe, e.g. for a cache of constant values. Rarely one value might be calculated twice, but that does not hurt as it is at the end be the same .
Even when the issues around memory safely are resolved, this is probably still not a good thing to rely on. What's really wanted here is a concurrent hash (along the lines of my Concurrent::Stack
and Concurrent::Queue
modules, which use lock-free algorithms). I did look at the papers on doing one of those; it turns out it's rather involved. However, those do give the kinds of properties you described (allowing benign recalculations).
@jnthn thanks a lot for the valuable insights. I have one last question: is it safe to do concurrent read access while adding one value to a hash? Or do I better lock also read access, eg. with ReadWriteLock?
In the internals, these situations are generally handled by a Lock
and the protect
method. In HLL code:
my $lock = Lock.new;
my %hash;
sub get_key($key) {
$lock.protect: { %hash{$key} }
}
sub set_key($key, $value) {
$lock.protect: { %hash{$key} = $value }
}
Can this issue be closed now?
Below you find a golfed unit test without external modules:
#!/usr/bin/env raku
use Test;
my $THREADS = 20;
my $ROUNDS = 10000;
{
my %hash;
my @workers = (^$THREADS).map: -> $num {
start {
for ^$ROUNDS -> $counter {
%hash{$num + $THREADS * $counter}++;
}
}
};
await @workers;
}
VM.request-garbage-collection;
pass 'no memory corruption when adding new hash keys concurrently without locking';
done-testing
If the issue can be closed? It is an open and confirmed bug which is only triggered when the programmer misbehaves :-) and thus understandably has little priority. If you wish I can move it over to the MoreVM project. Personally I would keep it open here, because there might also be other Raku programmers getting the same error and might be tempted to open a new bug. Like this it is documented what can be done or where to look closer. If the issue is closed, then it looks like it is not relevant any more. I think it should be closed when above test passes.
Also with the new hash implementation I get memory corruption, but differently ;-):
free(): invalid next size (normal)
Aborted (core dumped)
or
MoarVM panic: Internal error: invalid thread ID 65540 in GC work pass
or simply
Segmentation fault (core dumped)
(tested with v2020.10)
/me pings @nwc10
I do not have any insight in how to fix this.
@jnthn seems to have some idea on how to give "wrong" answers without crashing. I don't know enough about MoarVM concurrency (what is guaranteed, what one can get away with) to even know where to start on this.
(Putting locks around every hash lookup isn't going to fly, and my knowledge of "lock free data structures" is just those four words, as a search term. I think that the "single allocation" approach in MoarVM/MoarVM#1360 gets somewhere nearer to helping, as one can change it to "free at safe point", but that alone isn't everything, as it still leaves assumptions about visibility of memory ordering on multicore CPUs, which is not something I'm competent at)
I can't help here (and even work I have done is now stalled on review)
Is this still an issue?
v2024.09 brings
$ ./concurrent_unlocked_hash_access.raku
MoarVM oops: MVM_str_hash_entry_size called with a stale hashtable pointer
at ./concurrent_unlocked_hash_access.raku:11 (<ephemeral file>:)
from SETTING::src/core.c/Promise.rakumod:370 (/usr/share/perl6/runtime/CORE.c.setting.moarvm:)
from SETTING::src/core.c/ThreadPoolScheduler.rakumod:905 (/usr/share/perl6/runtime/CORE.c.setting.moarvm:)
from SETTING::src/core.c/ThreadPoolScheduler.rakumod:272 (/usr/share/perl6/runtime/CORE.c.setting.moarvm:)
from SETTING::src/core.c/ThreadPoolScheduler.rakumod:253 (/usr/share/perl6/runtime/CORE.c.setting.moarvm:)
from SETTING::src/core.c/ThreadPoolScheduler.rakumod:250 (/usr/share/perl6/runtime/CORE.c.setting.moarvm:run-one)
from SETTING::src/core.c/ThreadPoolScheduler.rakumod:291 (/usr/share/perl6/runtime/CORE.c.setting.moarvm:)
from SETTING::src/core.c/Thread.rakumod:69 (/usr/share/perl6/runtime/CORE.c.setting.moarvm:THREAD-ENTRY)
$
which is still a LTA error message
I think ae024a492f6c3ffb75cea0fab23943788d2fe5be was the commit that enabled str hashes to oops instead of segfault when a concurrent write is detected. What it does for str hashes might be able to port to the other types of hashes. Doing so won't improve the above situation, but would at least put all hash types on equal footing.
The Problem
My program aborts with
malloc
errors.Actual Behavior
The script in (source see next section) terminates with with
and the interesting part of the core dump shows:
before "golfing" it I have seen various other effects, like the process hanging,
or
Steps to Reproduce
run
which runs fine until we access the web interface to read the metrics several times
It fails the faster the more metric/label dimensions there are.
Environment
Manjaro Linux 20.0.2
perl6 -v
orraku -v
):2020.05