Repository navigation
Colossus bug fixes for 3.10.0 release - #5016
Conversation
…aders request handlers Fix for: Joystream#5002
…-fix-objectid-cache-bug
zeeshanakram3
left a comment
There was a problem hiding this comment.
Thanks for investigating and fixing the issue. Wouldn't it be much simpler to store data object ID (req.params.id) at the start of the both GET/HEAD handlers, before the try block starts. This way the data object ID in the finally block would still be valid and we won't have to separately do the unpinning at multiple different places. For example, following change fixes the bug
| if (!(await getDataObjectIdFromCache(dataObjectId))) { | ||
| sendResponseWithError(res, next, new WebApiError('File Not Found', 404), 'files') | ||
| return | ||
| } |
There was a problem hiding this comment.
Wouldn't this be redundant, since now pinDataObjectIdToCache ensures that ID exists before pinning it? If this is added to return a better error message in the response, then we can provide this error in as an argument of asset in pinDataObjectIdToCache function itself. e.g.
assert(idCache.has(dataObjectId), new WebApiError('File Not Found', 404))There was a problem hiding this comment.
We can also pass the Error as a second optional argument to the pinDataObjectIdToCache function
There was a problem hiding this comment.
Remember assertions don't throw and it is intended to fail the process completely. Meaning the caller needs to make sure they are calling at the right time. So if these new assertions fail it means our code is not correct.
Are you suggesting we should be returning/throwing an error/exception instead of these assertions?
There was a problem hiding this comment.
I picked assertions because I was treating the user of the idCache as "our own" code. So its not like a library.
With reference to https://softwareengineering.stackexchange.com/questions/15515/when-to-use-assertions-and-when-to-use-exceptions#15518
Let me know what you think.
There was a problem hiding this comment.
Remember assertions don't throw and it is intended to fail the process completely.
Maybe I am misunderstanding, but In javascript, the assert module throws the Error of type AssertionError, and we can actually pass a custom error too, that should be thrown in case assertion fails.
There was a problem hiding this comment.
You are absolutely correct. And given that is true I think I might be introducing more problems.
Let me think this through and come back to it.
Yes that was one of the reasons I moved the data object id instantiation outside of the It is easy to miss such cases when mixing |
987aa0d to
1983731
Compare
| // Skipping this check to allow re-upload of "lost" objects | ||
| if (dataObject.accepted.valueOf()) { | ||
| throw new WebApiError(`Data object ${dataObjectId} has already been accepted by storage node`, 400) | ||
| // throw new WebApiError(`Data object ${dataObjectId} has already been accepted by storage node`, 400) |
There was a problem hiding this comment.
Please remove this commented code.
| // assert(!idCache.has(dataObjectId)) | ||
| // existence check to avoid resetting pin count |
There was a problem hiding this comment.
Isn't it better to have this assertion?
| if (!flags.pendingFolder) { | ||
| logger.warn( | ||
| 'You did not specify a path to the pending directory. ' + | ||
| 'A pending folder under the uploads folder willl be used. ' + |
There was a problem hiding this comment.
| 'A pending folder under the uploads folder willl be used. ' + | |
| 'A pending folder under the uploads folder will be used. ' + |
| pendingFolder: flags.string({ | ||
| description: | ||
| 'Directory to store pending files which are uploaded upload (absolute path).\nIf not specified a subfolder under the uploads directory will be used.', | ||
| }), |
There was a problem hiding this comment.
I think making the temp folder configurable does not create any risks, but in the case of the pending folder what would happen if the operator specifies a different pendingFolder in different node runs? Would this lead to considering the objects in the previous pending folder as effectively lost (unless they were re-uploaded)
There was a problem hiding this comment.
Correct there is a risk of that happening. The operator can after start node with a new path for pending folder, simply copy the files to the new location and they will be picked up. But I think it is a necessary step towards making the uploads folder separate in preparation for external object storage back-end.
| (req: express.Request, res: express.Response<unknown, AppConfig>, next: NextFunction) => { | ||
| if (req.path === '/api/v1/files' && (req.method === 'GET' || req.method === 'HEAD')) { | ||
| validateDataObjectId(req, res) | ||
| .then(next) | ||
| .catch((error) => sendResponseWithError(res, next, error, 'upload')) | ||
| } else { | ||
| next() | ||
| } | ||
| }, |
There was a problem hiding this comment.
This middleware handler would never be invoked since req.path would always be /api/v1/files/{id}, i.e. it would include the data object ID as part of the path.
Also, it's it better to do this validation at the openApi schema level (e.g. example)?
There was a problem hiding this comment.
Good catch. Yeah I was surprised actually that the schema was for a string. Maybe it could be of type number rather than regex pattern on a string? Was a string perhaps chosen to support data object ids larger than javascript max number ?
There was a problem hiding this comment.
Even that regex expression could allow numbers too large for BN to fail.
With this custom function we could also do checks like if the id outside the range of valid dataobject ids?
|
|
||
| export function deleteDataObjectIdFromCache(dataObjectId: string): void { | ||
| assert(typeof dataObjectId === 'string') | ||
| // assert(idCache.has(dataObjectId)) |
There was a problem hiding this comment.
Isn't it better to have this assertion?
| sendResponseWithError(res, next, err, 'files') | ||
| } finally { | ||
| await unpinDataObjectIdFromCache(req.params.id) | ||
| // we assume that if stream.pipe throws, then stream.on('end') will never fire |
There was a problem hiding this comment.
I think the comment should not be here?
There was a problem hiding this comment.
The reason for the comment at both lines where we are invoking the unpin call is to explain that it is not a mistake (based on the assumption), and we should never actually get the result where unpin is invoked twice.
I suppose I could have a locally scoped function that will only even call unpinDataObjectIdFromCache() once and log a warning if it is every invoked multiple times as a signal that this assumption is false without causing a possible corrupt state or unpin throwing the assertion error.
There was a problem hiding this comment.
About this whole approach of pinning to prevent the cleanup service deleting a file being served, I may have overlooked that on linux file systems at least, it may not be necessary at all. I should have considered that when reviewing first PR that introduced the clean up service.
I do think we can safely get rid of this.
On top of that, I can't find the comment in the PR but I do remember at one point we were serving content from pending folder if objects was not yet moved to uploads, and I suggested that it was a bad idea as we didn't know what would happen if files was being moved while being served.
I think we should re-introduce ability to serve from pending folder. It will become more important when we move to object storage, if there is a delay in transferring the pending file from local file system to object storage (compared with what we do currently which is just renaming on the save disk volume).
What do you think?
There was a problem hiding this comment.
On top of that, I can't find the comment in the PR but I do remember at one point we were serving content from pending folder if objects was not yet moved to uploads, and I suggested that it was a bad idea as we didn't know what would happen if files was being being while being served.
This is the PR where you suggested and mage the change #4989
There was a problem hiding this comment.
Made unpinning less error prone and guaranteed to only happen once after pinning in getFile handler.
I read that end event on stream may/or may not fire if error event is fired, so there are potentially 3 places where we would need to unpin..
Done in d5ec0ec
There was a problem hiding this comment.
Made unpinning less error prone and guaranteed to only happen once after pinning in getFile handler.
Yeah, this is a good change actually.
| })().catch((err) => { | ||
| logger.error(`Accept pending objects service died: ${err}`) | ||
| }) |
There was a problem hiding this comment.
| })().catch((err) => { | |
| logger.error(`Accept pending objects service died: ${err}`) | |
| }) | |
| })() |
Since the async function is in try...catch, I don't think any promise rejections will ever be caught in .catch function. WDYT?
There was a problem hiding this comment.
You are correct, I think I was forced to add a catch handler because typescript complained. I'll try to use the previous code style to make this whole function cleaner. The reason I ended up with this was this was the point where I was debugging the issue which ended up being the invalid spread operator on the extrinsic result.
| logger.debug( | ||
| `Data object ${dataObject.id} in pending directory is no longer assigned to any of the upload buckets: ${this.uploadBuckets}.` | ||
| ) | ||
| await fsPromises.unlink(path.join(this.pendingDataObjectsDir, dataObject.id)) |
There was a problem hiding this comment.
I think we discussed that doing this is risky, and should probably by handled by pruning service (#4971 (comment))
So better to move such objects to the uploads folder, and pruning service can handle from there?
There was a problem hiding this comment.
Ah yes now I remember. That is a good idea actually.
| await Promise.allSettled( | ||
| pendingDataObjects.map(async (dataObject) => { | ||
| const storageBucket = dataObject.storageBag.storageBuckets.find(({ id }) => this.uploadBuckets.includes(id)) | ||
| // verify the ipfshash of the object matches against runtime! |
There was a problem hiding this comment.
@zeeshanakram3 what are your thoughts about doing this additional check here?
There was a problem hiding this comment.
This check is already being done after the file upload completes and before it is moved to the pending dir. So I think doing that would be redundant?
There was a problem hiding this comment.
It is of course redundant for objects that just came in through the http api, but not for files that might have been copied by operator directly. I'm thinking of scenario where operator maybe recovering their setup, moving files from other location after chaing pending folder path. Maybe somehow recovering "lost" objects from a backup/archive and placing them into the pending folder for processing..
There was a problem hiding this comment.
Yeah, in those cases it makes sense to have this validation. So maybe just add a comment, briefly explaining why are we doing this? In which scenarios it would be beneficial? As you described above
| (req: express.Request, res: express.Response<unknown, AppConfig>, next: NextFunction) => { | ||
| if (req.path.startsWith('/api/v1/files/') && (req.method === 'GET' || req.method === 'HEAD')) { | ||
| validateDataObjectId(req, res) | ||
| .then(next) | ||
| .catch((error) => sendResponseWithError(res, next, error, 'upload')) | ||
| } else { | ||
| next() | ||
| } | ||
| }, |
There was a problem hiding this comment.
Actually, this still won't work correctly as req.params inside validateDataObjectId is empty object ({}). This is because the express does not know about the structure of the URL yet (i.e. whether 1 in /api/v1/files/1 is parameter or part of the URL itself), and it will only know about the params after the next middleware (OpenApiValidator.middleware) is executed.
So unfortunately I think there is no way then doing this validation at openapi schema level
There was a problem hiding this comment.
Ah thanks for checking this.
So I'll drop it, and update the handlers to not assume the id has been validated fully, and at least update the schema validation.
closes:
undefinedkey into idCache map