1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-23 15:49:44 -05:00

Clean up HTTP async iterator code (denoland/deno_std#411)

Original: 68faf32f72
This commit is contained in:
Ryan Dahl 2019-05-20 09:17:26 -04:00 committed by GitHub
parent 227d92e046
commit a295bb0d42
7 changed files with 209 additions and 184 deletions

View file

@ -3,13 +3,12 @@ import { serve } from "./server.ts";
const addr = Deno.args[1] || "127.0.0.1:4500";
const server = serve(addr);
const body = new TextEncoder().encode("Hello World");
async function main(): Promise<void> {
console.log(`http://${addr}/`);
for await (const request of server) {
request.respond({ status: 200, body });
for await (const req of server) {
req.respond({ body });
}
}

View file

@ -1,55 +1,14 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
const { listen, copy, toAsyncIterator } = Deno;
type Listener = Deno.Listener;
type Conn = Deno.Conn;
type Reader = Deno.Reader;
type Writer = Deno.Writer;
import { BufReader, BufState, BufWriter } from "../io/bufio.ts";
import { TextProtoReader } from "../textproto/mod.ts";
import { STATUS_TEXT } from "./http_status.ts";
import { assert } from "../testing/asserts.ts";
interface Deferred {
promise: Promise<{}>;
resolve: () => void;
reject: () => void;
}
function deferred(isResolved = false): Deferred {
let resolve, reject;
const promise = new Promise(
(res, rej): void => {
resolve = res;
reject = rej;
}
);
if (isResolved) {
resolve();
}
return {
promise,
resolve,
reject
};
}
interface HttpConn extends Conn {
// When read by a newly created request B, lastId is the id pointing to a previous
// request A, such that we must wait for responses to A to complete before
// writing B's response.
lastPipelineId: number;
pendingDeferredMap: Map<number, Deferred>;
}
function createHttpConn(c: Conn): HttpConn {
const httpConn = Object.assign(c, {
lastPipelineId: 0,
pendingDeferredMap: new Map()
});
const resolvedDeferred = deferred(true);
httpConn.pendingDeferredMap.set(0, resolvedDeferred);
return httpConn;
}
import { assert, fail } from "../testing/asserts.ts";
import { deferred, Deferred, MuxAsyncIterator } from "../util/async.ts";
function bufWriter(w: Writer): BufWriter {
if (w instanceof BufWriter) {
@ -58,6 +17,7 @@ function bufWriter(w: Writer): BufWriter {
return new BufWriter(w);
}
}
export function setContentLength(r: Response): void {
if (!r.headers) {
r.headers = new Headers();
@ -74,6 +34,7 @@ export function setContentLength(r: Response): void {
}
}
}
async function writeChunkedBody(w: Writer, r: Reader): Promise<void> {
const writer = bufWriter(w);
const encoder = new TextEncoder();
@ -90,6 +51,7 @@ async function writeChunkedBody(w: Writer, r: Reader): Promise<void> {
const endChunk = encoder.encode("0\r\n\r\n");
await writer.write(endChunk);
}
export async function writeResponse(w: Writer, r: Response): Promise<void> {
const protoMajor = 1;
const protoMinor = 1;
@ -131,6 +93,7 @@ export async function writeResponse(w: Writer, r: Response): Promise<void> {
}
await writer.flush();
}
async function readAllIterator(
it: AsyncIterableIterator<Uint8Array>
): Promise<Uint8Array> {
@ -154,14 +117,14 @@ async function readAllIterator(
}
export class ServerRequest {
pipelineId: number;
url: string;
method: string;
proto: string;
headers: Headers;
conn: HttpConn;
conn: Conn;
r: BufReader;
w: BufWriter;
done: Deferred<void> = deferred();
public async *bodyStream(): AsyncIterableIterator<Uint8Array> {
if (this.headers.has("content-length")) {
@ -244,134 +207,102 @@ export class ServerRequest {
}
async respond(r: Response): Promise<void> {
// Check and wait if the previous request is done responding.
const lastPipelineId = this.pipelineId - 1;
const lastPipelineDeferred = this.conn.pendingDeferredMap.get(
lastPipelineId
);
assert(!!lastPipelineDeferred);
await lastPipelineDeferred.promise;
// If yes, delete old deferred and proceed with writing.
this.conn.pendingDeferredMap.delete(lastPipelineId);
// Write our response!
await writeResponse(this.w, r);
// Signal the next pending request that it can start writing.
const currPipelineDeferred = this.conn.pendingDeferredMap.get(
this.pipelineId
);
assert(!!currPipelineDeferred);
currPipelineDeferred.resolve();
// Signal that this request has been processed and the next pipelined
// request on the same connection can be accepted.
this.done.resolve();
}
}
interface ServeEnv {
reqQueue: ServerRequest[];
serveDeferred: Deferred;
}
/** Continuously read more requests from conn until EOF
* Calls maybeHandleReq.
* bufr is empty on a fresh TCP connection.
* Would be passed around and reused for later request on same conn
* TODO: make them async function after this change is done
* https://github.com/tc39/ecma262/pull/1250
* See https://v8.dev/blog/fast-async
*/
async function readRequest(
c: HttpConn,
bufr?: BufReader
conn: Conn,
bufr: BufReader
): Promise<[ServerRequest, BufState]> {
if (!bufr) {
bufr = new BufReader(c);
}
const bufw = new BufWriter(c);
const req = new ServerRequest();
// Set and incr pipeline id;
req.pipelineId = ++c.lastPipelineId;
// Set a new pipeline deferred associated with this request
// for future requests to wait for.
c.pendingDeferredMap.set(req.pipelineId, deferred());
req.conn = c;
req.r = bufr!;
req.w = bufw;
const tp = new TextProtoReader(bufr!);
let s: string;
req.conn = conn;
req.r = bufr;
req.w = new BufWriter(conn);
const tp = new TextProtoReader(bufr);
let err: BufState;
// First line: GET /index.html HTTP/1.0
[s, err] = await tp.readLine();
let firstLine: string;
[firstLine, err] = await tp.readLine();
if (err) {
return [null, err];
}
[req.method, req.url, req.proto] = s.split(" ", 3);
[req.method, req.url, req.proto] = firstLine.split(" ", 3);
[req.headers, err] = await tp.readMIMEHeader();
return [req, err];
}
function maybeHandleReq(
env: ServeEnv,
conn: Conn,
maybeReq: [ServerRequest, BufState]
): void {
const [req, _err] = maybeReq;
if (_err) {
conn.close(); // assume EOF for now...
return;
export class Server implements AsyncIterable<ServerRequest> {
private closing = false;
constructor(public listener: Listener) {}
close(): void {
this.closing = true;
this.listener.close();
}
env.reqQueue.push(req); // push req to queue
env.serveDeferred.resolve(); // signal while loop to process it
}
function serveConn(env: ServeEnv, conn: HttpConn, bufr?: BufReader): void {
readRequest(conn, bufr).then(maybeHandleReq.bind(null, env, conn));
}
// Yields all HTTP requests on a single TCP connection.
private async *iterateHttpRequests(
conn: Conn
): AsyncIterableIterator<ServerRequest> {
const bufr = new BufReader(conn);
let bufStateErr: BufState;
let req: ServerRequest;
export async function* serve(
addr: string
): AsyncIterableIterator<ServerRequest> {
const listener = listen("tcp", addr);
const env: ServeEnv = {
reqQueue: [], // in case multiple promises are ready
serveDeferred: deferred()
};
// Routine that keeps calling accept
let handleConn = (_conn: Conn): void => {};
let scheduleAccept = (): void => {};
const acceptRoutine = (): void => {
scheduleAccept = (): void => {
listener.accept().then(handleConn);
};
handleConn = (conn: Conn): void => {
const httpConn = createHttpConn(conn);
serveConn(env, httpConn); // don't block
scheduleAccept(); // schedule next accept
};
scheduleAccept();
};
acceptRoutine();
// Loop hack to allow yield (yield won't work in callbacks)
while (true) {
await env.serveDeferred.promise;
env.serveDeferred = deferred(); // use a new deferred
let queueToProcess = env.reqQueue;
env.reqQueue = [];
for (const result of queueToProcess) {
yield result;
// Continue read more from conn when user is done with the current req
// Moving this here makes it easier to manage
serveConn(env, result.conn, result.r);
while (!this.closing) {
[req, bufStateErr] = await readRequest(conn, bufr);
if (bufStateErr) break;
yield req;
// Wait for the request to be processed before we accept a new request on
// this connection.
await req.done;
}
if (bufStateErr === "EOF") {
// The connection was gracefully closed.
} else if (bufStateErr instanceof Error) {
// TODO(ry): send something back like a HTTP 500 status.
} else if (this.closing) {
// There are more requests incoming but the server is closing.
// TODO(ry): send a back a HTTP 503 Service Unavailable status.
} else {
fail(`unexpected BufState: ${bufStateErr}`);
}
conn.close();
}
listener.close();
// Accepts a new TCP connection and yields all HTTP requests that arrive on
// it. When a connection is accepted, it also creates a new iterator of the
// same kind and adds it to the request multiplexer so that another TCP
// connection can be accepted.
private async *acceptConnAndIterateHttpRequests(
mux: MuxAsyncIterator<ServerRequest>
): AsyncIterableIterator<ServerRequest> {
if (this.closing) return;
// Wait for a new connection.
const conn = await this.listener.accept();
// Try to accept another connection and add it to the multiplexer.
mux.add(this.acceptConnAndIterateHttpRequests(mux));
// Yield the requests that arrive on the just-accepted connection.
yield* this.iterateHttpRequests(conn);
}
[Symbol.asyncIterator](): AsyncIterableIterator<ServerRequest> {
const mux: MuxAsyncIterator<ServerRequest> = new MuxAsyncIterator();
mux.add(this.acceptConnAndIterateHttpRequests(mux));
return mux.iterate();
}
}
export function serve(addr: string): Server {
const listener = listen("tcp", addr);
return new Server(listener);
}
export async function listenAndServe(

View file

@ -22,31 +22,6 @@ const dec = new TextDecoder();
type Handler = () => void;
interface Deferred {
promise: Promise<{}>;
resolve: Handler;
reject: Handler;
}
function deferred(isResolved = false): Deferred {
let resolve: Handler = (): void => void 0;
let reject: Handler = (): void => void 0;
const promise = new Promise(
(res, rej): void => {
resolve = res;
reject = rej;
}
);
if (isResolved) {
resolve();
}
return {
promise,
resolve,
reject
};
}
const responseTests: ResponseTest[] = [
// Default response
{
@ -72,8 +47,8 @@ test(async function responseWrite(): Promise<void> {
const buf = new Buffer();
const bufw = new BufWriter(buf);
const request = new ServerRequest();
request.pipelineId = 1;
request.w = bufw;
request.conn = {
localAddr: "",
remoteAddr: "",
@ -86,13 +61,12 @@ test(async function responseWrite(): Promise<void> {
write: async (): Promise<number> => {
return -1;
},
close: (): void => {},
lastPipelineId: 0,
pendingDeferredMap: new Map([[0, deferred(true)], [1, deferred()]])
close: (): void => {}
};
await request.respond(testCase.response);
assertEquals(buf.toString(), testCase.raw);
await request.done;
}
});

View file

@ -16,6 +16,7 @@ import "./strings/test.ts";
import "./testing/test.ts";
import "./textproto/test.ts";
import "./toml/test.ts";
import "./util/test.ts";
import "./ws/test.ts";
import "./testing/main.ts";

85
util/async.ts Normal file
View file

@ -0,0 +1,85 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
// TODO(ry) It'd be better to make Deferred a class that inherits from
// Promise, rather than an interface. This is possible in ES2016, however
// typescript produces broken code when targeting ES5 code.
// See https://github.com/Microsoft/TypeScript/issues/15202
// At the time of writing, the github issue is closed but the problem remains.
export interface Deferred<T> extends Promise<T> {
resolve: (value?: T | PromiseLike<T>) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
reject: (reason?: any) => void;
}
/** Creates a Promise with the `reject` and `resolve` functions
* placed as methods on the promise object itself. It allows you to do:
*
* const p = deferred<number>();
* // ...
* p.resolve(42);
*/
export function deferred<T>(): Deferred<T> {
let methods;
const promise = new Promise<T>(
(resolve, reject): void => {
methods = { resolve, reject };
}
);
return Object.assign(promise, methods) as Deferred<T>;
}
interface TaggedYieldedValue<T> {
iterator: AsyncIterableIterator<T>;
value: T;
}
/** The MuxAsyncIterator class multiplexes multiple async iterators into a
* single stream. It currently makes a few assumptions:
* - The iterators do not throw.
* - The final result (the value returned and not yielded from the iterator)
* does not matter; if there is any, it is discarded.
*/
export class MuxAsyncIterator<T> implements AsyncIterable<T> {
private iteratorCount = 0;
private yields: Array<TaggedYieldedValue<T>> = [];
private signal: Deferred<void> = deferred();
add(iterator: AsyncIterableIterator<T>): void {
++this.iteratorCount;
this.callIteratorNext(iterator);
}
private async callIteratorNext(
iterator: AsyncIterableIterator<T>
): Promise<void> {
const { value, done } = await iterator.next();
if (done) {
--this.iteratorCount;
} else {
this.yields.push({ iterator, value });
}
this.signal.resolve();
}
async *iterate(): AsyncIterableIterator<T> {
while (this.iteratorCount > 0) {
// Sleep until any of the wrapped iterators yields.
await this.signal;
// Note that while we're looping over `yields`, new items may be added.
for (let i = 0; i < this.yields.length; i++) {
const { iterator, value } = this.yields[i];
yield value;
this.callIteratorNext(iterator);
}
// Clear the `yields` list and reset the `signal` promise.
this.yields.length = 0;
this.signal = deferred();
}
}
[Symbol.asyncIterator](): AsyncIterableIterator<T> {
return this.iterate();
}
}

34
util/async_test.ts Normal file
View file

@ -0,0 +1,34 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { test, runIfMain } from "../testing/mod.ts";
import { assertEquals } from "../testing/asserts.ts";
import { MuxAsyncIterator, deferred } from "./async.ts";
test(async function asyncDeferred(): Promise<void> {
const d = deferred<number>();
d.resolve(12);
});
async function* gen123(): AsyncIterableIterator<number> {
yield 1;
yield 2;
yield 3;
}
async function* gen456(): AsyncIterableIterator<number> {
yield 4;
yield 5;
yield 6;
}
test(async function asyncMuxAsyncIterator(): Promise<void> {
const mux = new MuxAsyncIterator<number>();
mux.add(gen123());
mux.add(gen456());
const results = new Set();
for await (const value of mux) {
results.add(value);
}
assertEquals(results.size, 6);
});
runIfMain(import.meta);

View file

@ -1 +1,2 @@
import "./async_test.ts";
import "./deep_assign_test.ts";