-
Notifications
You must be signed in to change notification settings - Fork 3
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
Logging Metadata #52
Logging Metadata #52
Conversation
private val refurbish: (T) -> Unit | ||
) { | ||
private val lock = reentrantLock() | ||
private val cache = ArrayDeque<T>() |
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.
Does the queue (as apposed to an atomic
) protect against someone logging from within a log lambda?
Otherwise, would an atomic
have sufficed?
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.
Re. ArrayDeque
, I just default to it instead of List
when I'm doing stack/queue operations, since it's more explicit. For stack operations (like here) it's basically the same.
Re. atomic
, it doesn't work for the high throughput, low-allocation goal of this pool. In DispatchLogger
, for example, we're using consumers.update { it + consumer }
-- which allocates a new list and copies all the plus the new one (I'm not worried about the O(N^2) inefficiency here, because N is super small and setup only happens once). We have to do that copy for two reasons.
update
is loop based. It gets the current value, performs the update function, then does acompareAndSet
to make sure nothing else changed the value. This means that the block can be called more than once, so it's not safe to mutate the value. But even if it wasn't a loop...atomic
is basically a pointer. While it's okay to change what it points at (from any thread), in Kotlin/Native the thing pointed at is still frozen. This would also prevent us from mutating a list.
The solution here, albeit ugly, should be fast on every platform. Effectively...
- On JVM: we're using one pool shared by all threads. Getting a value from the pool and returning a value to it holds the lock, but everything else can live outside it. Realistically, there should be very little contention and when there is, it should resolve itself quickly.
- On JS: the lock is implemented as a no-op.
- On Native: the
Pool
itself is@ThreadLocal
, so thelock
/cache
are each per-thread. As such there will never be any contention on thelock
and it will always happy-path, and thecache
won't freeze.
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.
Thanks for the incredibly thorough explanation. Super clever approach with the @ThreadLocal
. 👍
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.
And yet, somehow, despite writing a small essay, I managed to miss the original question.
It should be safe to nest log calls, both inside a Logger
(although it will need conditional logic, or else it will infinitely loop) and within a Log
call such as Log.verbose { Log.debug { "a" }; "b" }
. I'll admit I haven't tested either of these cases.
Codecov Report
@@ Coverage Diff @@
## main #52 +/- ##
============================================
- Coverage 94.50% 94.40% -0.11%
- Complexity 41 52 +11
============================================
Files 11 13 +2
Lines 91 125 +34
Branches 8 8
============================================
+ Hits 86 118 +32
- Misses 5 6 +1
- Partials 0 1 +1
Continue to review full report at Codecov.
|
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.
Thoroughly impressed with how elegant of an API and streamlined the internals of this are! Really awesome stuff! 💯 🚀 🤯
import kotlin.native.concurrent.ThreadLocal | ||
|
||
@ThreadLocal // Thread local pool means that metadata returned from it are safe to mutate on that same thread. | ||
private val metadataPool = Pool(factory = ::Metadata, refurbish = Metadata::clear) |
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.
TIL of ::<class_name>
constructor syntax, so cool!
Example Use
Breaking Changes
Logger
will need to add a new parameter.Logger
will need to supply metadata.Log.xyz
will likely work (in source), but there are some edge cases where they'll need to be updated (such as if you pass a function reference, instead of lambda).