-
Notifications
You must be signed in to change notification settings - Fork 31
feat: add c binding #1322
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
base: master
Are you sure you want to change the base?
feat: add c binding #1322
Conversation
proc sendRequestToCodexThread*( | ||
ctx: ptr CodexContext, | ||
reqType: RequestType, | ||
reqContent: pointer, |
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.
Hm... how will this work when we have to upload data to Codex? I suppose reqContent is meant to be a string or byte array (and not, say, a file descriptor), so we will probably have to constrain loading to happen from files and pass a local path here? Also wondering what'll be the best approach for a download API...
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, I didn't think deeply to that yet, but maybe we could have some kind of steam mode where the reqContent
is a Stream
or Reader
, so the worker will read small chunks of the files until it is fully uploaded.
And for the download, opposite direction with Writer
.
I am not sure of how to do it with Nim + C binding, but in Javascript I would try to consume the data in that way.
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.
reqContent
is more for describing the codex-local operation (eg start node).
userData
I believe is what should be used for passing data. It is a pointer,
which could point to a file path or a seq of bytes (assuming it is not copied).
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.
reqContent
is more for describing the codex-local operation (eg start node).userData
I believe is what should be used for passing data. It is a pointer, which could point to a file path or a seq of bytes (assuming it is not copied).
If you look at sendRequestToCodexThread
, the third parameter is the reqContent
, which is in the case of Lifecycle request :
NodeLifecycleRequest.createShared(
NodeLifecycleMsgType.CREATE_NODE, configJson
),
It creates a request shared object that contains the json config provided by the Go application. So I think the reqContent
contains the actual data passed by the caller.
The userData
is a bit confusing. It looks like a context that will be used when the Go app receives the callback. It is not intended to be used by the Nim code, but by the Go code when the callback is invoked.
You can have a global userData
context plus a userData
context per call. I’m not sure how useful this is for Codex right now.
# between threads assumes that there aren't concurrent requests. | ||
# Rearchitecting the signaling + migrating to a MP Channel will allow us to receive | ||
# requests concurrently and spare us the need of locks | ||
lock: Lock |
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.
Not sure this is a big deal as requests run async and the actual sending of the request to the Codex thread should be relatively fast, plus this shouldn't be a very "hot" API anyway?
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.
Yes. The only issue arises if you have multiple application threads, meaning multiple calls to sendRequestToCodexThread
. In that case, the request channel may receive several requests, but currently the worker thread only processes one request per wakeup and sends an ACK to the first application thread:
# Pop a request from the channel
let recvOk = ctx.reqChannel.tryRecv(request)
if not recvOk:
error "codex thread could not receive a request"
continue
So we could be in a situation where the second application thread could end up waiting indefinitely for an ACK from the Codex thread.
of LIFECYCLE: | ||
cast[ptr NodeLifecycleRequest](request[].reqContent).process(codex) | ||
|
||
handleRes(await retFut, request) |
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.
Hmmm... does this mean we are calling back the C code directly from the Chronos thread?
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.
Yes
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.
To summarize the discussion during the meeting:
The callbacks are currently executed on the Chronos thread (= working thread). This can be an issue if a callback is slow, because the Chronos thread will block and other requests cannot be processed.
One solution would be to introduce a dispatcher thread with a queue for events and a dedicated thread that dequeues them and runs the callbacks.
Another potential approach would be to move to a Multi Producer channel instead of the current Single Producer channel. This would allow multiple threads to enqueue requests concurrently, so even if one thread is blocked by a callback, others can continue processing requests.
deallocShared(request) | ||
|
||
if res.isErr(): | ||
foreignThreadGc: |
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? Isn't handleRes
being called from the Chronos thread?
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.
Well, I’m not sure that’s always the case, especially when a Go routine is involved.
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 mean from what I understood, Go routines will run threads that are considered as foreign
by Nim so that my be an issue with the GC.
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.
Definitely looks like it's going in the right direction! I tried it out locally and it worked well!
Just a few suggestions/small comments even though I know this is a draft. I can imagine you were saving exception handling for "clean up". Best for last right 😂
let count = parseInt(input) | ||
if count != 0 and count < 2: | ||
warn "Invalid number of threads", input = input | ||
proc parse*(T: type ThreadCount, p: string): Result[ThreadCount, string] = |
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 probably pull all these out into a parser module, so that you can import the parser module from conf
and egnode_lifecycle_request
. This will prevent the need to import conf
from node_lifecycle_request
.
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 will still need to import CodexConf
from conf
. But yeah I can import only CodexConf
and move the parsing method into a parser.nim as you suggested.
try: | ||
updateLogLevel(config.logLevel) | ||
except ValueError as err: | ||
try: | ||
stderr.write "Invalid value for --log-level. " & err.msg & "\n" | ||
except IOError: | ||
echo "Invalid value for --log-level. " & err.msg | ||
quit QuitFailure |
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.
Isn't this already called in setupLogging
?
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 removed it from the setupLogging
because the compiler was complaining about gcsafe
issue.
library/codex_context.nim
Outdated
cast[CodexCallback](ctx[].eventCallback)( | ||
RET_OK, unsafeAddr event[0], cast[csize_t](len(event)), ctx[].eventUserData | ||
) | ||
except Exception, CatchableError: |
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'm assuming that Exception had to be caught to get this to compile? Could be that there is no way to avoid Defects being raised in the call path.
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.
Well, I am not sure this template is not called yet :). But I will remove the Exception for now and add it back if it is needed.
proc readValue*(r: var JsonReader, val: var MultiAddress) = | ||
val = MultiAddress.init(r.readValue(string)).get() | ||
|
||
proc readValue*(r: var JsonReader, val: var NatConfig) = |
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 feels like these deserialisations are actually quite generic and should maybe live elsewhere. In ethers, we redirect all de/serializations to nim-serde instead:
import pkg/serde/json
## Generic conversions to use nim-serde instead of nim-json-serialization for
## json rpc serialization purposes
## writeValue => `%`
## readValue => fromJson
proc writeValue*[T: not JsonNode](
writer: var JsonWriter[JrpcConv],
value: T) {.raises:[IOError].} =
writer.writeValue(%value)
proc readValue*[T: not JsonNode](
r: var JsonReader[JrpcConv],
result: var T) {.raises: [SerializationError, IOError].} =
var json = r.readValue(JsonNode)
result = T.fromJson(json).getOrRaise(SerializationError)
proc sendRequestToCodexThread*( | ||
ctx: ptr CodexContext, | ||
reqType: RequestType, | ||
reqContent: pointer, |
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.
reqContent
is more for describing the codex-local operation (eg start node).
userData
I believe is what should be used for passing data. It is a pointer,
which could point to a file path or a seq of bytes (assuming it is not copied).
library/codex_context.nim
Outdated
## process proc. See the 'codex_thread_request.nim' module for more details. | ||
ok() | ||
|
||
proc runCodex(ctx: ptr CodexContext) {.async.} = |
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.
Probably should also not raise errors inside this proc otherwise it may silently
crash the thread?
library/libcodex.nim
Outdated
return callback.success("", userData) | ||
|
||
proc codex_upload_init( | ||
ctx: ptr CodexContext, filepath: cstring, callback: CodexCallback, userData: pointer |
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.
What happens with the filepath if you're uploading via chunks? Is it not used?
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.
Well, the filepath can be obviously the absolute file path when you upload a file.
But when you upload with chunks, it is used as filename
. Then the node.store
will use it for the filename and the mimetype.
…ze and the last chunk
Just opening a draft PR to get early reviews