Skip to content
This repository has been archived by the owner on Aug 18, 2018. It is now read-only.

Use http server over local domain socket instead of json-socket #3

Closed
wants to merge 9 commits into from

Conversation

etamponi
Copy link
Member

The code isn't great because it's my first attempt at TypeScript, but:

  • It now uses an http server which connects over the same unix/windows socket used by json-socket
  • I had to use express because fastify had issues during shutdown

Moreover, I found that the same problem that afflicted json-socket was causing issues with the http servers as well (included fastify): at the end I found that we were overloading the socket. In the retryRequest function you'll see that I put a simple backoff timeout if there are too many requests in flight. Probably we can port this backoff code back to json-socket so that it works too, if you like it more than the http approach.

@etamponi
Copy link
Member Author

A couple more things worth mentioning:

  1. I didn't run the test suite because I didn't know how to do that;
  2. in retryRequest, the catch block is never taken, because the backoff that I put in front of the request makes sure all the requests run fine. You can try increasing the threshold number of inflight requests to see the errors happening.

@zkochan zkochan self-requested a review December 18, 2017 23:27
package.json Outdated
"json-socket": "^0.3.0",
"express": "^4.16.2",
"body-parser": "^1.18.2",
"fastify": "^0.35.7",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it doesn't seem like you use fastify in code

setTimeout(resolve, 100)
}).then(() => {
return retryRequest(options)
})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can use p-limit

import JsonSocket = require('json-socket')
import net = require('net')

import request = require('request-promise-native')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use node-fetch instead

err,
}, (merr) => merr && console.error(merr))
})
function garbageCollectResponses (packageResponses: object, msgId: string) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can have 2 separate dictionaries and you won't need this function

limit: '10mb',
}))

app.post('/requestPackage', async (request: Request, response: Response) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of the separate actions, it might be faster to have a single route with a switch inside.

@etamponi
Copy link
Member Author

I made the following changes:

  • Removed express, now using the raw http server and a switch clause -- it supports errors too now
  • Added p-limit
  • Split packageResponses into two separate dictionaries

The only thing I didn't do is replacing request-promise-native with node-fetch, as I wasn't able to figure out how node-fetch handles error conditions. The docs say it would throw a FetchError, but in my tests: https://glitch.com/edit/#!/fastify-test?path=server.js:56:15 it does not happen.

Is node-fetch better, or can we leave request-promise-native as it seems it is working quite well? (We also use it on the Glitch source code without any issues).

@zkochan
Copy link
Member

zkochan commented Dec 19, 2017 via email

@etamponi
Copy link
Member Author

etamponi commented Dec 19, 2017 via email

@zkochan
Copy link
Member

zkochan commented Dec 19, 2017 via email

@etamponi
Copy link
Member Author

etamponi commented Dec 19, 2017

Unfortunately I couldn't use node-fetch because it does not support unix sockets, apparently just for the sake of not supporting them. The node-fetch maintainer suggests using got instead, so I replaced request-promise-native with got.

You're right: request was causing some issues, including the random ECONNRESET errors. With got, in theory, we can remove the limit on the number of inflight requests. I think it is better to leave it though.

@etamponi
Copy link
Member Author

etamponi commented Dec 19, 2017

With the last commit I made the tests pass on my local machine, I hope travis is happy too :)

UPDATE: looks like Node 4 doesn't like my changes, I don't know why...

UPDATE 2: Fixed tests on Node 4 too :)

@zkochan
Copy link
Member

zkochan commented Dec 19, 2017

Well done! I'll review this tonight

package.json Outdated
@@ -38,7 +38,11 @@
"@pnpm/logger": "^1.0.0",
"@pnpm/npm-resolver": "^0.3.1",
"@pnpm/tarball-fetcher": "^0.3.1",
"@types/got": "^7.1.6",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type dependencies of prod dependencies should be also prod dependencies

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

} else {
remotePrefix = `http://${initOpts.hostname}:${initOpts.port}`
}
const limitedRetryFetch = retryFetch.bind(null, pLimit(100))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd make the concurrency configurable

if (initOpts.path) {
remotePrefix = `http://unix:${initOpts.path}:`
} else {
remotePrefix = `http://${initOpts.hostname}:${initOpts.port}`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be fine to pass in the formed path, these manipulations can be done by pnpm

method: 'POST',
})
return JSON.parse(response.body)
}).catch((e) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can use async/await syntax here with a regular try/catch

also, I think the try/catch should be inside the function and cover just the got() call, as errors by JSON.parse will always be passed through

if (!e.message.startsWith('Error: connect ECONNRESET') && !e.message.startsWith('Error: connect ECONNREFUSED')) {
throw e
}
return retryFetch(limit, url, body)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got() supports retries out of the box, so I guess this logic is no longer needed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and is there a way to tell got to retry as many times as it's needed?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, by default it reties 2 times
https://www.npmjs.com/package/got#retries

await store.saveState()
return

switch (req.url) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the switch statement should be probably executed early, because if an unknown route is requested, there's no need to read the data

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The switch cannot be executed early because we don't have the body until the request has been received, and I don't see any reason to add complexity to wait for the body since we always need the body or it is empty so it doesn't add overhead.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't the URL known from the very start? The URL can be validated before collecting the body

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and once we validate the URL? We've to wait for the body... so I am not really sure why we should validate the URL first

case '/saveState':
await store.saveState()
res.end(JSON.stringify('OK'))
break
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if no route was matched, I think a 404 response should be returned

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right 👍

@zkochan
Copy link
Member

zkochan commented Dec 19, 2017

It might be a good idea to prefix the routes with a version, like /v0/requestPackage. In that case we'll be able to make breaking changes more smoothly. Though it is not necessary in scope of this PR. The feature is experimental for now, anyway.

@zkochan
Copy link
Member

zkochan commented Dec 19, 2017

Where are the changes in shrinkwrap.yaml?

@etamponi
Copy link
Member Author

etamponi commented Dec 19, 2017

EDIT: I was using npm install, I'll use pnpm install to generate it

@zkochan
Copy link
Member

zkochan commented Dec 19, 2017

It is updated automatically when you install/uninstall with pnpm.

@etamponi
Copy link
Member Author

Just pushed the fixes following your suggestions:

  • now using retries in got, setting it to 1000 so that we're sure we retry enough times; got rid of the catch block
  • pLimit argument is now configurable through initOpts.concurrency, defaulting to 100
  • updated shrinkwrap.yaml
  • initOpts now has remotePrefix, the manipulations need to be done in pnpm now
  • the request body is now fetched in a promise, bodyPromise, and we do the URL validation before the body has been fully received. We then await for the bodyPromise when needed.
  • added a 404 catch-all route

method: 'POST',
retries: 1000, // picking a very large number to be sure it keeps retrying as long as needed
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure this is a good idea because, with each retry, the delay is increased. So eventually it will just hang up. It can be considered a silent error

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll pass a function with a fixed delay instead

@zkochan
Copy link
Member

zkochan commented Dec 19, 2017

Sorry, last 2 requests.

  1. @types/node should be prod dependency as well
  2. unite the changes to one commit and name it, using our conventions: feat: use http over local domain socket for communication

@etamponi etamponi closed this Dec 19, 2017
@etamponi
Copy link
Member Author

I closed this PR and created a new one with the changes and commit you asked

@zkochan
Copy link
Member

zkochan commented Dec 20, 2017

These changes don't work with Windows. The previous implementation worked on Windows

@etamponi
Copy link
Member Author

What exactly doesn't work? Probably the remotePrefix string needs to be written correctly for Windows

@zkochan
Copy link
Member

zkochan commented Dec 20, 2017

Yes, that might be the case, as the URL contains http://unix: https://github.com/pnpm/pnpm/blob/next/src/cmd/server.ts#L34

@etamponi
Copy link
Member Author

I don't have access to a Windows machine unfortunately, I can ask a coworker later on though. In the meantime, it should be possible to use localhost connections on Windows instead of a socket.

@zkochan
Copy link
Member

zkochan commented Dec 20, 2017

We have appveyor tests on windows. They fail now https://ci.appveyor.com/project/zkochan/pnpm-17nv8/build/2015

I can also look into it, but only in the evening

@zkochan
Copy link
Member

zkochan commented Dec 20, 2017

I fixed it by using localhost connection but it may have to be changed back to IPC in the future, for consistency and speed, on all systems

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants