[1] https://openjdk.org/jeps/441 [2] https://openjdk.org/jeps/444 [3] https://openjdk.org/jeps/430
714 karma · joined May 5, 2016
hn@zak.knill.dev
[1] https://openjdk.org/jeps/441 [2] https://openjdk.org/jeps/444 [3] https://openjdk.org/jeps/430
> 1. I'm talking about C# and Java
I write Java in vim, and it's pretty usable. Coc.nvim and coc-java fix code completion. There are also plugins to make running tests, and the debugger usable too. I can't stand the start-up time of IntelliJ!
I see "explanation" and "how-to" conflated so regularly, as a quickstart how-to guide falls into a tangent of explaining why something works a certain way.
I've not directly worked on it, but I get the impression that it's mostly used in file backup because of how Differential Sync works. In Differential Sync the server and the client both hold two copies of the data/document. One representing their own state, and one representing what they thing the other's state is. Then the differences are computed between those documents and sent to the other.
For any given client, this isn't too onerous. You'd have to hold the client and server copy of the doc. But for a server, serving many clients, you'll end up with a lot of client-specific copies of that document. For N clients, you'd have N+1 copies of the document. It doesn't take too many clients before this becomes a real problem.
So if you're backing up 1 client to 1 server, Differential Sync is fine. But if you're trying to keep tens, hundreds, or thousands of clients in sync then you end up with a lot of client "shadow" copies of the server's document.
It's a little unfair to describe locking as "[giving] up on UX", especially given some well known collaboration products use it quite successfully; Google Sheets cell locking, Miro element/Text locking, etc.
Ofc, it's going to depend on the scope of what's being locked. These two examples are quite finely scoped element locks.
Yeah, sure, you can build a secondary index over the data. But you're still having to decode the blob and index it. There's no version where you can index the data without using the library to expose the underlying model state (like you could if you weren't using a CRDT).
On locking, yes, it's hard. But it's not the same kind of locking that you'd expect in other parts of a system. You're locking the UI, not the actual data, so it's a tiny bit more forgiving. In general the locks aren't trying to force consistency, instead they are trying to prompt the humans to reduce the chance of conflict happening in the first place. Ofc, you still have to care about the locking, unlocking, disconnection problems, etc.
Here's a decent example/demo you can play around with in multiple windows:
https://examples.ably.dev/component-locking?space=W0V-t5oY6A...
I'm thinking a lot about keeping server and client data in sync while working on our hopefully-soon-to-be-released LiveSync product[1]
LWW becomes useful if you can:
a) help humans to see who is doing what on a doc
b) reduce the size of the change that is LWW
As you've said:
> However our LWW texts are individually small - one block/paragraph in size
This is really important, because by reducing the size of the individual element that any single user is updating you can also reduce the chance of conflicts. With a small amount of contextual feedback (like the notion icon next to the block) a lot of the problem cases are just avoided.
Clearly locking and updating the entire document would be terrible, but if you can do it on a small enough scope that others can change other elements, it can work really well.
(If you've worked on the notion things you're describing then I'm sure you know this better than I do, but just spelling it out really clearly.)
If you've ever used one of those awful early-2000s Microsoft word collaboration systems where you have to check-out the doc, and remember to check it back in, and no one can use it until you've checked it back in... it's awful!
I'm not directly in this team, but one of the teams at my company have been working on this problem. They call it "Spaces", and one of the features solves this component locking problem.
Take the example from the article:
> Please look into this for me. Do not, DO NOT, spend more than 20 minutes on this. Please come back with whatever you have after 20 minutes.
Each person looking at this is going to achieve a slightly different amount in 20 mins. Even if we ignore that, there's no guarantee that the manager making the request is going to get what they need from 20 minutes of your effort.
In 'agile' working environments, I regularly see discovery/research tasks labelled as a 'spike' and given a 'time-box'. I often question: "What happens if we don't discover what we want to in that amount of time?", and there's generally no good answer. What actually tends to happen is the 'time-box' is expanded until we found out what we needed to. So time is a terrible proxy for effort, substance, or scope. The same is true here, the manager has some idea of what they want to find out, and you better hope you can do that amount of research in their sized time-box.
I think the later examples on this article are better:
> Are you expecting something super thorough, [...] or a quick write up?
This is much more valuable because we're talking in terms of the scope of work, not how long it will take. When anyone measures in time, there's no guarantee that you'll get what you want in that amount of time, and then what? Well, we add more time!
I think the collaboration experiences that you see in Figma, Miro, Notion, etc should _just be possible_ for all apps. And for a lot of product development teams they probably look at what would be involved to try and build a realtime collaboration experience and think it's just gonna be either too hard, or too much work. But the 'art of the possible' as definitely moved on (as evidenced by the list of companies in the blog post that are addressing these challenges).
As a bunch of the other commenters point out, the hard-part isn't just building the code for the UI that will show collaboration experiences, the hard-part is often handling the stateful connections (websockets) and making sure the messages are delivered to the right clients. You either build a stateless/polling mechanism over HTTP, or you have to deal with trying to scale and load-balance websocket connections. Neither approach is particularly easy.
Handle(context.Context, Record) error
This method has access to the context, which means you can get the logging handler to extract values from the context. Instead of storing the logger on the context, you can extract the traceId, etc values from the context and log those.It's a little bit involved to write a whole logger from scratch, but you can 'wrap' the existing logger handlers and include values from the context relatively easily.
There are examples in this project (which aims to help solve your usecase): https://github.com/zknill/slogmw
If you want to use slog, I can imagine setting up the logger with a handler specific for the CLI output in the CLI tool, and a json or text structured handler in the webservice app.
The quesiton is, do you actually want structured logging in the CLI app? Yes you probably want to print something out, but is it _structured_ in the sense that slog expects? Or is it just some output.
If it's not really structured, then you probably want some other interface/library that better represents the logging you want to do. Slog will push you towards structured key-value pairs, and you might find yourself fighting against this in the CLI app.
It's easy to get started with log/slog and one of the built in handlers, but as soon as you want to change something the library design pushes you towards implementing an entire handler.
For example, if I want the built in JSON format, but with a different formatting of the Time field, that's not easy to do. It's not obvious how to change the built in handler.
I wrote slogmw[1] to solve this problem. It's a set of middleware and examples that make it easy to make small changes to the built in handlers without having to write a whole new handler from scratch.
It's easy to get started with log/slog and one of the built in handlers, but as soon as you want to change something the library design pushes you towards implementing an entire handler.
For example, if I was the built in JSON format, but with a different formatting of the Time field, that's not easy to do. It's not obvious how to change the built in handler.
I wrote slogmw[1] to solve this problem. It's a set of middleware and examples that make it easy to make small changes to the built in handlers without having to write a whole new handler from scratch.
Firstly (1); "I want to use the ORM for everything (table definitions, indexes, and queries)"
Then second (2), on the other extreme: "I don't want an ORM, I want to do everything myself, all the SQL and reading the data into objects".
Then thirdly (3) the middle ground: "I want the ORM to do the boring reading/writing data between the database and the code's objects".
The problem with ORMs is that they are often designed to do number 1, and are used to do number 3. This means there's often 'magic' in the ORM, when really all someone wanted to do was generate the code to read/write data from the database. In my experience this pushes engineers to adopt number 2.
I'm a big fan of projects like sqlc[1] which will take SQL that you write, and generate the code for reading/writing that data/objects into and out of the database. It gives you number 3 without any of the magic from number 1.
In protocol version 2 (introduced in postgres 14) large in-progress transactions can appear in the new "Stream" messages sent on the replication connection: StreamStart, StreamStop, StreamAbort, StreamCommit, etc. In the case of large in-progress transactions uncommitted data might end up in the replication connection after a StreamStart message. But you would also receive a StreamCommit or StreamAbort message to tell you what happened to that transaction.
I've not worked out what qualifies as a "large" transaction though. But it is _possible_ to get uncommitted data in the replication connection, although unlikely.
https://www.postgresql.org/docs/15/protocol-logicalrep-messa...
i.e. a stream of messages like: "BEGIN", "[the data]", ["COMMIT" or "ROLLBACK"].
So any application that listens to the Postgres replication protocol can handle the transaction in the same way that Postgres does. Concretely you might choose to open a SQLite transaction on BEGIN, apply the statements, and then COMMIT or ROLLBACK based on the next messages received on the stream replication protocol.
The data sent on the replication protocol includes the state of the row after the write query has completed. This means you don't need to worry about getting out of sync on queries like "UPDATE field = field + 1" because you have access to the exact resulting value as stored by Postgres.
TL;DR - you can follow the same begin/change/commit flow that the original transaction did on the upstream Postgres server, and you have access to the exact underlying data after the write was committed.
It's also true (as other commenters have pointed out) that for not-huge transactions (i.e. not streaming transactions, new feature in Postgres 15) the BEGIN message will only be sent if the transaction was committed. It's pretty unlikely that you will ever process a ROLLBACK message from the protocol (although possible).
This means writes are eventually consistent, currently, but I intend to include a feature that allows waiting for that write to be reflected back in SQLite which would satisfy the 'read your own writes' property.
SQLedge will never be in a situation where the SQLite database thinks it has a write, but that write is yet to be applied to the upstream Postgres server.
Basically, the Postgres server 'owns' the writes, and can handle them just like it would if SQLedge didn't exist.
This slightly breaks model of having a local copy serve data faster, but if only the minority of queries use a format that we don't understand in SQLite then only that minority of queries will suffer from the full latency to the main PG server.
The database is using the read in the log as the point-in-time where that read happened. The writes before that point will be viewed by the read, and the writes after won't be.
By sending the read to the leader, when we receive that read back, we know for sure that all the writes that should have been replicated to our local copy have been replicated (because our read has made the full round-trip that all our writes would have, so we're guaranteed to have received all the writes before that read's "point-in-time").
[1] https://fly.io/docs/litefs/proxy/ [2] https://docs.turso.tech/reference/data-consistency#on-the-re...
And I think there are other approaches to replication that still retain the embedded aspect; litefs, litestream.
I guess I'm thinking, if it looks like postgres, and it speaks postgres (wire protocol) and it has features or replication like postgres, why wouldn't you use postgres?
It seems like this project removes that benefit, so what is it solving for or adding instead? What's the upside?
If you try it out, I'd love to get your feedback.
I built something similar for working with Java code, but it could be extended to other languages.
I've seen engineers find it hard to work out if their code is organised well together. So I built this tool to show them visually.
I wonder if the OP used ChatGPT to help them write this post.