4 ms·
Think of it like a GPU compute shader. Like a shader, you have to submit "the whole thing" to the kernel at once, so that it has the program available all at on
by derefr 5y ago
Think of it like a GPU compute shader. Like a shader, you have to submit "the whole thing" to the kernel at once, so that it has the program available all at once in its own context, and can guarantee it won't get blocked waiting for your app to submit "the rest of it."
To enable the client program to submit "the whole thing", you really need to represent that sequence of operations as some kind of instruction stream loaded into a buffer — either a text-based one (source code in some language), or bytecode in some ISA.
(Why not capture a sequence of API operations that do nothing when called, but instead build such an instruction stream; where "committing" them actually runs the instruction stream? Y'know, like a Redis pipeline? Because a "pipeline" like that can't branch based on the result of executing part of it, unless branching/looping/etc are also implemented as operations in this hypothetical fluent-API-DSL. At which point, you're just programming with the semantics of another language, with extra steps.)
And if a static analysis pass is going to occur to check the code for runtime complexity, then you're already transforming the input into a different verified output with an intermediate AST-like structure; so at that point you may as well accept a text-based format and expose a "compile" system call. Just as GPU drivers do for compute shaders.
I'm guessing that there's probably an internal ZFS API allowing you to instead pass in some kind of bytecode (LuaJIT bytecode?) and just have it directly static-analyzed and plopped into a kernel handle, rather than parsed, then static-analyzed, then codegen'ed. They'd definitely need something like that for testing.
- josephg 5y agoThis is a tricky problem, but it’s something all good transactional databases solve. For example, the way this is handled in foundationdb is that you create a transaction, then within that transaction issue a series of read and write calls. The read calls are executed immediately, and they see the database as if other concurrent changes weren’t happening. The write calls get batched up and at the end of the transaction block they are submitted together - with information about which keys were read. On submission, the transaction block can fail with E_RETRY - which means a conflict happened, the transaction was entirely aborted and the userland program should run the transaction again from the start. I’d absolutely adore an API like this for filesystem operations. There are all sorts of half baked, dirty workarounds to get around the lack of atomicity in posix filesystems. Eg, that whole “write a file then rename it” thing - which almost nobody implements correctly because it’s error prone and most correct solutions on top of posix have terrible performance.
- derefr 5y agoI don't think you understand the point of the constraints ZFS has put in place here. They're not there for transactional isolation per se. They're there because these ZFS maintenance operations run as synchronous system calls that take per-filesystem global mutex locks. Blocking within one, with those locks acquired, and thunking back down to userspace, would mean that 1. a CPU core, and 2. the filesystem itself, would both be tied up indefinitely until that callback thunk returns. If it ever does! Picture a database server where once you begin a transaction, the whole DB server goes single-threaded and doesn't serve any other clients until the TX completes. Fundamentally, at least for its more arcane global operations, that's exactly what a filesystem is. Usually this doesn't matter, because even these arcane filesystem operations take on the order of microseconds to complete (just with lots of non-locked pre/post execution overhead that inflates the total syscall time.) But a userspace program can do arbitrary-much stuff in a callback. It can even just get into an infinite loop, and never get back to the kernel. This is why there isn't such a thing as a system call with userspace callbacks! (Or any API equivalent to one—e.g. one with tx_begin + tx_commit calls, where tx_begin acquires a global kernel lock before returning to userspace, under the expectation that userspace call tx_commit to release the kernel lock.) On the other hand, submitting a "callback program" as a whole to the kernel, allows the kernel to statically analyze the behavior of the "callback program" in advance; and only if it is determined to be "simple" (e.g. if it predictably halts after a short time as a static property), will the kernel issue the program a capability for actually running that "callback program." This is how compute shaders work. This is how eBPF works. And this is (apparently) how this ZFS Lua thing works. They all do it for similar reasons: to ensure that the program is a complete, soft-realtime practical unit of work to be executing in some bounded-time context. You do get atomicity from this, but it's not MVCC atomicity. It's more like Redis's atomicity: during the execution of a ZFS Lua program, all other clients making system calls against the filesystem will wait on acquiring the filesystem mutex, and so will block until the program completes. There's never a case where other activity might "interleave" with the Lua program's execution, and cause it to retry. There is no other activity. The only case in which the Lua program can actually fail (and so get rolled back), is if it dies/is killed in the middle of execution, due to e.g. a hardware power cut. That's the kind of atomicity that ZFS is concerned about—guaranteeing that changes that make it into the filesystem journal, are complete valid change units. In this case, one Lua program is one change unit in the journal.