Implement wire reconnect support

This commit is contained in:
luaneko 2025-01-11 06:02:32 +11:00
parent 6f9e9770cf
commit da7f7e12f3
Signed by: luaneko
GPG Key ID: 406809B8763FF07A

283
wire.ts
View File

@ -1,5 +1,6 @@
import { import {
type BinaryLike, type BinaryLike,
buf_concat,
buf_concat_fast, buf_concat_fast,
buf_eq, buf_eq,
buf_xor, buf_xor,
@ -7,7 +8,9 @@ import {
from_base64, from_base64,
from_utf8, from_utf8,
jit, jit,
type Receiver,
semaphore, semaphore,
type Sender,
to_base64, to_base64,
to_utf8, to_utf8,
TypedEmitter, TypedEmitter,
@ -475,57 +478,38 @@ export interface Channel
} }
export async function wire_connect(options: WireOptions) { export async function wire_connect(options: WireOptions) {
const { host, port } = options; const wire = new Wire(options);
const wire = new Wire(await socket_connect(host, port), options); return await wire.connect(), wire;
return await wire.connected, wire;
}
async function socket_connect(hostname: string, port: number) {
if (hostname.startsWith("/")) {
const path = join(hostname, `.s.PGSQL.${port}`);
return await Deno.connect({ transport: "unix", path });
} else {
const socket = await Deno.connect({ transport: "tcp", hostname, port });
return socket.setNoDelay(), socket.setKeepAlive(), socket;
}
} }
export class Wire extends TypedEmitter<WireEvents> implements Disposable { export class Wire extends TypedEmitter<WireEvents> implements Disposable {
readonly #socket;
readonly #params; readonly #params;
readonly #auth; readonly #connect;
readonly #connected;
readonly #query; readonly #query;
readonly #begin; readonly #begin;
readonly #listen; readonly #listen;
readonly #notify; readonly #notify;
readonly #close; readonly #close;
get socket() {
return this.#socket;
}
get params() { get params() {
return this.#params; return this.#params;
} }
get connected() { constructor(options: WireOptions) {
return this.#connected;
}
constructor(socket: Deno.Conn, options: WireOptions) {
super(); super();
({ ({
params: this.#params, params: this.#params,
auth: this.#auth, connect: this.#connect,
query: this.#query, query: this.#query,
begin: this.#begin, begin: this.#begin,
listen: this.#listen, listen: this.#listen,
notify: this.#notify, notify: this.#notify,
close: this.#close, close: this.#close,
} = wire_impl(this, socket, options)); } = wire_impl(this, options));
this.#socket = socket; }
(this.#connected = this.#auth()).catch(close);
connect() {
return this.#connect();
} }
query<T = Row>(sql: SqlFragment): Query<T>; query<T = Row>(sql: SqlFragment): Query<T>;
@ -579,51 +563,66 @@ export class Wire extends TypedEmitter<WireEvents> implements Disposable {
} }
} }
const msg_PD = object({ P: Parse, D: Describe }); async function socket_connect(hostname: string, port: number) {
const msg_BE = object({ B: Bind, E: Execute }); if (hostname.startsWith("/")) {
const msg_BEc = object({ B: Bind, E: Execute, c: CopyDone }); const path = join(hostname, `.s.PGSQL.${port}`);
const msg_BEcC = object({ B: Bind, E: Execute, c: CopyDone, C: Close }); return await Deno.connect({ transport: "unix", path });
} else {
const socket = await Deno.connect({ transport: "tcp", hostname, port });
return socket.setNoDelay(), socket.setKeepAlive(), socket;
}
}
function wire_impl( function wire_impl(
wire: Wire, wire: Wire,
socket: Deno.Conn, { host, port, user, database, password, runtime_params, types }: WireOptions
{ user, database, password, runtime_params, types }: WireOptions
) { ) {
// current runtime parameters as reported by postgres
const params: Parameters = Object.create(null); const params: Parameters = Object.create(null);
function log(level: LogLevel, ctx: object, msg: string) { function log(level: LogLevel, ctx: object, msg: string) {
wire.emit("log", level, ctx, msg); wire.emit("log", level, ctx, msg);
} }
// wire supports re-connection; socket and read/write channels are null when closed
let socket: Deno.Conn | null = null;
let read_pop: Receiver<Uint8Array> | null = null;
let write_push: Sender<Uint8Array> | null = null;
async function read<T>(type: Encoder<T>) { async function read<T>(type: Encoder<T>) {
const msg = await read_recv(); const msg = read_pop !== null ? await read_pop() : null;
if (msg === null) throw new WireError(`connection closed`); if (msg !== null) return ser_decode(type, msg_check_err(msg));
else return ser_decode(type, msg_check_err(msg)); else throw new WireError(`connection closed`);
} }
async function read_raw() { async function read_msg() {
const msg = await read_recv(); const msg = read_pop !== null ? await read_pop() : null;
if (msg === null) throw new WireError(`connection closed`); if (msg !== null) return msg;
else return msg; else throw new WireError(`connection closed`);
} }
async function* read_socket() { // socket reader channel worker
const buf = new Uint8Array(64 * 1024); async function read_socket(socket: Deno.Conn, push: Sender<Uint8Array>) {
for (let n; (n = await socket.read(buf)) !== null; ) const header_size = 5;
yield buf.subarray(0, n); const read_buf = new Uint8Array(64 * 1024); // shared buffer for all socket reads
let buf = new Uint8Array(); // concatenated messages read so far
for (let read; (read = await socket.read(read_buf)) !== null; ) {
buf = buf_concat_fast(buf, read_buf.subarray(0, read)); // push read bytes to buf
while (buf.length >= header_size) {
const size = ser_decode(Header, buf).length + 1;
if (buf.length < size) break;
const msg = buf.subarray(0, size); // shift one message from buf
buf = buf.subarray(size);
if (!handle_msg(msg)) push(msg);
}
} }
const read_recv = channel.receiver<Uint8Array>(async function read(send) { // there should be nothing left in buf if we gracefully exited
let err: unknown; if (buf.length !== 0) throw new WireError(`unexpected end of stream`);
try { }
let buf = new Uint8Array();
for await (const chunk of read_socket()) {
buf = buf_concat_fast(buf, chunk);
for (let n; (n = ser_decode(Header, buf).length + 1) <= buf.length; ) {
const msg = buf.subarray(0, n);
buf = buf.subarray(n);
function handle_msg(msg: Uint8Array) {
switch (msg_type(msg)) { switch (msg_type(msg)) {
// https://www.postgresql.org/docs/current/protocol-flow.html#PROTOCOL-ASYNC // https://www.postgresql.org/docs/current/protocol-flow.html#PROTOCOL-ASYNC
case NoticeResponse.type: { case NoticeResponse.type: {
@ -631,7 +630,7 @@ function wire_impl(
const notice = new PostgresError(fields); const notice = new PostgresError(fields);
log(severity_level(notice.severity), notice, notice.message); log(severity_level(notice.severity), notice, notice.message);
wire.emit("notice", notice); wire.emit("notice", notice);
continue; return true;
} }
case ParameterStatus.type: { case ParameterStatus.type: {
@ -643,7 +642,7 @@ function wire_impl(
value, value,
}); });
wire.emit("parameter", name, value, prev); wire.emit("parameter", name, value, prev);
continue; return true;
} }
case NotificationResponse.type: { case NotificationResponse.type: {
@ -653,33 +652,55 @@ function wire_impl(
); );
wire.emit("notify", channel, payload, process_id); wire.emit("notify", channel, payload, process_id);
channels.get(channel)?.emit("notify", payload, process_id); channels.get(channel)?.emit("notify", payload, process_id);
continue; return true;
} }
} }
send(msg); return false;
} }
}
if (buf.length !== 0) throw new WireError(`unexpected end of stream`);
} catch (e) {
throw ((err = e), e);
} finally {
wire.emit("close", err);
}
});
function write<T>(type: Encoder<T>, value: T) { function write<T>(type: Encoder<T>, value: T) {
return write_raw(ser_encode(type, value)); write_msg(ser_encode(type, value));
} }
async function write_raw(buf: Uint8Array) { function write_msg(buf: Uint8Array) {
if (write_push !== null) write_push(buf);
else throw new WireError(`connection closed`);
}
// socket writer channel worker
async function write_socket(socket: Deno.Conn, pop: Receiver<Uint8Array>) {
for (let buf; (buf = await pop()) !== null; ) {
const bufs = [buf]; // proactively dequeue more queued msgs synchronously, if any
for (let i = 1, buf; (buf = pop.try()) !== null; ) bufs[i++] = buf;
if (bufs.length !== 1) buf = buf_concat(bufs); // write queued msgs concatenated, reduce write syscalls
for (let i = 0, n = buf.length; i < n; ) for (let i = 0, n = buf.length; i < n; )
i += await socket.write(buf.subarray(i)); i += await socket.write(buf.subarray(i));
} }
}
async function connect() {
using _rlock = await rlock();
using _wlock = await wlock();
close(new WireError(`reconnecting`));
try {
const s = (socket = await socket_connect(host, port));
read_pop = channel.receiver((push) => read_socket(s, push));
write_push = channel.sender((pop) => write_socket(s, pop));
await handle_auth(); // run auth with rw lock
} catch (e) {
throw (close(e), e);
}
}
function close(reason?: unknown) { function close(reason?: unknown) {
socket.close(), read_recv.close(reason); socket?.close(), (socket = null);
read_pop?.close(reason), (read_pop = null);
write_push?.close(reason), (write_push = null);
for (const name of Object.keys(params))
delete (params as Record<string, string>)[name];
st_cache.clear(), (st_ids = 0);
(tx_status = "I"), (tx_stack.length = 0);
} }
// https://www.postgresql.org/docs/current/protocol-flow.html#PROTOCOL-FLOW-PIPELINING // https://www.postgresql.org/docs/current/protocol-flow.html#PROTOCOL-FLOW-PIPELINING
@ -697,13 +718,13 @@ function wire_impl(
} }
async function pipeline_read<T>(r: () => T | PromiseLike<T>) { async function pipeline_read<T>(r: () => T | PromiseLike<T>) {
using _rlock = await rlock(); using _lock = await rlock();
try { try {
return await r(); return await r();
} finally { } finally {
try { try {
let msg; let msg;
while (msg_type((msg = await read_raw())) !== ReadyForQuery.type); while (msg_type((msg = await read_msg())) !== ReadyForQuery.type);
({ tx_status } = ser_decode(ReadyForQuery, msg)); ({ tx_status } = ser_decode(ReadyForQuery, msg));
} catch { } catch {
// ignored // ignored
@ -712,12 +733,12 @@ function wire_impl(
} }
async function pipeline_write<T>(w: () => T | PromiseLike<T>) { async function pipeline_write<T>(w: () => T | PromiseLike<T>) {
using _wlock = await wlock(); using _lock = await wlock();
try { try {
return await w(); return await w();
} finally { } finally {
try { try {
await write(Sync, {}); write(Sync, {});
} catch { } catch {
// ignored // ignored
} }
@ -725,11 +746,9 @@ function wire_impl(
} }
// https://www.postgresql.org/docs/current/protocol-flow.html#PROTOCOL-FLOW-START-UP // https://www.postgresql.org/docs/current/protocol-flow.html#PROTOCOL-FLOW-START-UP
async function auth() { async function handle_auth() {
using _rlock = await rlock(); // always run within rw lock (see connect())
using _wlock = await wlock(); write(StartupMessage, {
await write(StartupMessage, {
version: 196608, version: 196608,
params: { params: {
application_name: "pglue", application_name: "pglue",
@ -744,7 +763,7 @@ function wire_impl(
}); });
auth: for (;;) { auth: for (;;) {
const msg = msg_check_err(await read_raw()); const msg = msg_check_err(await read_msg());
switch (msg_type(msg)) { switch (msg_type(msg)) {
case NegotiateProtocolVersion.type: { case NegotiateProtocolVersion.type: {
const { bad_options } = ser_decode(NegotiateProtocolVersion, msg); const { bad_options } = ser_decode(NegotiateProtocolVersion, msg);
@ -762,7 +781,7 @@ function wire_impl(
throw new WireError(`kerberos authentication is deprecated`); throw new WireError(`kerberos authentication is deprecated`);
case 3: // AuthenticationCleartextPassword case 3: // AuthenticationCleartextPassword
await write(PasswordMessage, { password }); write(PasswordMessage, { password });
continue; continue;
case 5: // AuthenticationMD5Password case 5: // AuthenticationMD5Password
@ -778,7 +797,7 @@ function wire_impl(
// AuthenticationSASL // AuthenticationSASL
case 10: case 10:
await auth_sasl(); await handle_auth_sasl();
continue; continue;
default: default:
@ -786,8 +805,9 @@ function wire_impl(
} }
} }
// wait for ready
ready: for (;;) { ready: for (;;) {
const msg = msg_check_err(await read_raw()); const msg = msg_check_err(await read_msg());
switch (msg_type(msg)) { switch (msg_type(msg)) {
case BackendKeyData.type: case BackendKeyData.type:
continue; // ignored continue; // ignored
@ -797,11 +817,18 @@ function wire_impl(
break ready; break ready;
} }
} }
// re-listen previously registered channels
await Promise.all(
channels
.keys()
.map((name) => query(sql`listen ${sql.ident(name)}`).execute())
);
} }
// https://www.postgresql.org/docs/current/sasl-authentication.html#SASL-SCRAM-SHA-256 // https://www.postgresql.org/docs/current/sasl-authentication.html#SASL-SCRAM-SHA-256
// https://datatracker.ietf.org/doc/html/rfc5802 // https://datatracker.ietf.org/doc/html/rfc5802
async function auth_sasl() { async function handle_auth_sasl() {
const bits = 256; const bits = 256;
const hash = `SHA-${bits}`; const hash = `SHA-${bits}`;
const mechanism = `SCRAM-${hash}`; const mechanism = `SCRAM-${hash}`;
@ -858,7 +885,7 @@ function wire_impl(
)}`; )}`;
const client_first_message_bare = `${username},${initial_nonce}`; const client_first_message_bare = `${username},${initial_nonce}`;
const client_first_message = `${gs2_header}${client_first_message_bare}`; const client_first_message = `${gs2_header}${client_first_message_bare}`;
await write(SASLInitialResponse, { mechanism, data: client_first_message }); write(SASLInitialResponse, { mechanism, data: client_first_message });
const server_first_message_str = from_utf8( const server_first_message_str = from_utf8(
(await read(AuthenticationSASLContinue)).data (await read(AuthenticationSASLContinue)).data
@ -877,7 +904,7 @@ function wire_impl(
const client_proof = buf_xor(client_key, client_signature); const client_proof = buf_xor(client_key, client_signature);
const proof = `p=${to_base64(client_proof)}`; const proof = `p=${to_base64(client_proof)}`;
const client_final_message = `${client_final_message_without_proof},${proof}`; const client_final_message = `${client_final_message_without_proof},${proof}`;
await write(SASLResponse, { data: client_final_message }); write(SASLResponse, { data: client_final_message });
const server_key = await hmac(salted_password, "Server Key"); const server_key = await hmac(salted_password, "Server Key");
const server_signature = await hmac(server_key, auth_message); const server_signature = await hmac(server_key, auth_message);
@ -897,29 +924,28 @@ function wire_impl(
readonly name = `__st${st_ids++}`; readonly name = `__st${st_ids++}`;
constructor(readonly query: string) {} constructor(readonly query: string) {}
parse_task: Promise<{ #parse_task: Promise<{
ser_params: ParameterSerializer; ser_params: ParameterSerializer;
Row: RowConstructor; Row: RowConstructor;
}> | null = null; }> | null = null;
parse() { parse() {
return (this.parse_task ??= this.#parse()); return (this.#parse_task ??= this.#parse());
} }
async #parse() { async #parse() {
try { try {
const { name, query } = this; const { name, query } = this;
return await pipeline( return await pipeline(
() => () => {
write(msg_PD, { write(Parse, { statement: name, query, param_types: [] });
P: { statement: name, query, param_types: [] }, write(Describe, { which: "S", name });
D: { which: "S", name }, },
}),
async () => { async () => {
await read(ParseComplete); await read(ParseComplete);
const ser_params = make_param_ser(await read(ParameterDescription)); const ser_params = make_param_ser(await read(ParameterDescription));
const msg = msg_check_err(await read_raw()); const msg = msg_check_err(await read_msg());
const Row = const Row =
msg_type(msg) === NoData.type msg_type(msg) === NoData.type
? EmptyRow ? EmptyRow
@ -929,13 +955,13 @@ function wire_impl(
} }
); );
} catch (e) { } catch (e) {
throw ((this.parse_task = null), e); throw ((this.#parse_task = null), e);
} }
} }
portals = 0; #portals = 0;
portal() { portal() {
return `${this.name}_${this.portals++}`; return `${this.name}_${this.#portals++}`;
} }
} }
@ -944,6 +970,7 @@ function wire_impl(
(params: unknown[]): (string | null)[]; (params: unknown[]): (string | null)[];
} }
// makes function to serialize query parameters
function make_param_ser({ param_types }: ParameterDescription) { function make_param_ser({ param_types }: ParameterDescription) {
return jit.compiled<ParameterSerializer>`function ser_params(xs) { return jit.compiled<ParameterSerializer>`function ser_params(xs) {
return [ return [
@ -960,6 +987,7 @@ function wire_impl(
new (columns: (BinaryLike | null)[]): Row; new (columns: (BinaryLike | null)[]): Row;
} }
// makes function to create Row objects
const EmptyRow = make_row_ctor({ columns: [] }); const EmptyRow = make_row_ctor({ columns: [] });
function make_row_ctor({ columns }: RowDescription) { function make_row_ctor({ columns }: RowDescription) {
const Row = jit.compiled<RowConstructor>`function Row(xs) { const Row = jit.compiled<RowConstructor>`function Row(xs) {
@ -998,7 +1026,7 @@ function wire_impl(
stdout: WritableStream<Uint8Array> | null stdout: WritableStream<Uint8Array> | null
) { ) {
for (let rows = [], i = 0; ; ) { for (let rows = [], i = 0; ; ) {
const msg = msg_check_err(await read_raw()); const msg = msg_check_err(await read_msg());
switch (msg_type(msg)) { switch (msg_type(msg)) {
default: default:
case DataRow.type: case DataRow.type:
@ -1034,7 +1062,7 @@ function wire_impl(
if (stream !== null) { if (stream !== null) {
const writer = stream.getWriter(); const writer = stream.getWriter();
try { try {
for (let msg; msg_type((msg = await read_raw())) !== CopyDone.type; ) { for (let msg; msg_type((msg = await read_msg())) !== CopyDone.type; ) {
const { data } = ser_decode(CopyData, msg_check_err(msg)); const { data } = ser_decode(CopyData, msg_check_err(msg));
await writer.write(to_utf8(data)); await writer.write(to_utf8(data));
} }
@ -1046,7 +1074,7 @@ function wire_impl(
writer.releaseLock(); writer.releaseLock();
} }
} else { } else {
while (msg_type(msg_check_err(await read_raw())) !== CopyDone.type); while (msg_type(msg_check_err(await read_msg())) !== CopyDone.type);
} }
} }
@ -1055,16 +1083,16 @@ function wire_impl(
const reader = stream.getReader(); const reader = stream.getReader();
try { try {
for (let next; !(next = await reader.read()).done; ) for (let next; !(next = await reader.read()).done; )
await write(CopyData, { data: next.value }); write(CopyData, { data: next.value });
await write(CopyDone, {}); write(CopyDone, {});
} catch (e) { } catch (e) {
await write(CopyFail, { cause: String(e) }); write(CopyFail, { cause: String(e) });
throw e; throw e;
} finally { } finally {
reader.releaseLock(); reader.releaseLock();
} }
} else { } else {
await write(CopyDone, {}); write(CopyDone, {});
} }
} }
@ -1076,13 +1104,10 @@ function wire_impl(
log("debug", { query: query }, `executing simple query`); log("debug", { query: query }, `executing simple query`);
const { chunks, err } = await pipeline( const { chunks, err } = await pipeline(
async () => { () => (write(QueryMessage, { query }), write_copy_in(stdin)),
await write(QueryMessage, { query });
return write_copy_in(stdin);
},
async () => { async () => {
for (let chunks = [], err; ; ) { for (let chunks = [], err; ; ) {
const msg = await read_raw(); const msg = await read_msg();
switch (msg_type(msg)) { switch (msg_type(msg)) {
default: default:
case ReadyForQuery.type: case ReadyForQuery.type:
@ -1134,23 +1159,16 @@ function wire_impl(
try { try {
const { rows, tag } = await pipeline( const { rows, tag } = await pipeline(
async () => { async () => {
const B = { write(Bind, {
portal, portal,
statement: st.name, statement: st.name,
param_formats: [], param_formats: [],
param_values, param_values,
column_formats: [], column_formats: [],
}; });
const E = { portal, row_limit: 0 }; write(Execute, { portal, row_limit: 0 });
const C = { which: "P" as const, name: portal };
if (stdin !== null) {
await write(msg_BE, { B, E });
await write_copy_in(stdin); await write_copy_in(stdin);
return write(Close, C); write(Close, { which: "P" as const, name: portal });
} else {
return write(msg_BEcC, { B, E, c: {}, C });
}
}, },
async () => { async () => {
await read(BindComplete); await read(BindComplete);
@ -1193,22 +1211,16 @@ function wire_impl(
try { try {
let { done, rows, tag } = await pipeline( let { done, rows, tag } = await pipeline(
async () => { () => {
const B = { write(Bind, {
portal, portal,
statement: st.name, statement: st.name,
param_formats: [], param_formats: [],
param_values, param_values,
column_formats: [], column_formats: [],
}; });
const E = { portal, row_limit: chunk_size }; write(Execute, { portal, row_limit: chunk_size });
if (stdin !== null) {
await write(msg_BE, { B, E });
return write_copy_in(stdin); return write_copy_in(stdin);
} else {
return write(msg_BEc, { B, E, c: {} });
}
}, },
async () => { async () => {
await read(BindComplete); await read(BindComplete);
@ -1358,7 +1370,7 @@ function wire_impl(
} }
}; };
return { params, auth, query, begin, listen, notify, close }; return { params, connect, query, begin, listen, notify, close };
} }
export interface PoolOptions extends WireOptions { export interface PoolOptions extends WireOptions {
@ -1517,9 +1529,8 @@ function pool_impl(
}; };
async function connect() { async function connect() {
const { host, port } = options; const wire = new PoolWire(options);
const wire = new PoolWire(await socket_connect(host, port), options); await wire.connect(), all.add(wire);
await wire.connected, all.add(wire);
const { connection_id } = wire; const { connection_id } = wire;
return wire return wire
.on("log", (l, c, s) => pool.emit("log", l, { ...c, connection_id }, s)) .on("log", (l, c, s) => pool.emit("log", l, { ...c, connection_id }, s))