-
Notifications
You must be signed in to change notification settings - Fork 26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(api): availabilities patch endpoint #692
Conversation
0044060
to
b4e5e9a
Compare
65f3061
to
0706677
Compare
We should report the bug in presto |
totalSize* {.serialize.}: UInt256 | ||
freeSize* {.serialize.}: UInt256 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have decided to split the "size" tracking property into two parts "total size" and "free size", where the second indicates how much is left from the availability.
While the totalSize
could be calculated from size + sum(reservation.size)
(which is the direction I originally took), it seemed as more KISS approach to simply have that extra property.
codex/utils/options.nim
Outdated
Option[input] | ||
|
||
|
||
template Optionalize*(t: typed): untyped = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I originally planned to include an option to add the serialize
pragma to the newly constructed object type. However, I realized that it is not necessary for the JSON deserialization use case I was targeting. It can be incorporated in the future if the need arises.
x.availabilityId == y.availabilityId and | ||
x.size == y.size and | ||
x.requestId == y.requestId and | ||
x.slotIndex == y.slotIndex | ||
x.id == y.id | ||
proc `==`*(x, y: Availability): bool = | ||
x.id == y.id and | ||
x.size == y.size and | ||
x.duration == y.duration and | ||
x.maxCollateral == y.maxCollateral and | ||
x.minPrice == y.minPrice | ||
x.id == y.id |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@emizzle I have changed the equal compersion to use only the ID as IMHO that is what ID is for. Do you think it might break something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that should be ok. I'm not entirely sure if I remember correctly, but I think maybe ids didn't exist when Availability
was first created, hence the need for comparing the properties.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a very vague recollection of just an id
comparison not being enough, but for the life of me, I can't remember why 😅 I guess we'll find out one way or the other 😂
For sure, I already reported it: status-im/nim-presto#77 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice! I like the way you addressed the PATCH functionality, very elegant.
bea520a
to
e32106b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome job Adam, lots of great work here!
if error of NotExistsError: | ||
return RestApiResponse.error(Http404, "Availability not found") | ||
else: | ||
return RestApiResponse.error(Http500, error.msg) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is used in multiple places, could we extract this into a template for better readability? eg
if error of NotExistsError: | |
return RestApiResponse.error(Http404, "Availability not found") | |
else: | |
return RestApiResponse.error(Http500, error.msg) | |
template availabilityNotFound(error) = | |
if error of NotExistsError: | |
RestApiResponse.error(Http404, "Availability not found") | |
else: | |
RestApiResponse.error(Http500, error.msg) | |
return availabilityNotFound(error) |
except CatchableError as exc: | ||
trace "Excepting processing request", exc = exc.msg | ||
return RestApiResponse.error(Http500) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could also do a templated block instead of wrapping it all in try/catch. eg
template onError(body) =
try:
body
except CatchableError as exc:
trace "Excepting processing request", exc = exc.msg
return RestApiResponse.error(Http500)
onError:
without contracts =? node.contracts.host:
return RestApiResponse.error(Http503, "Sales unavailable")
....
x.availabilityId == y.availabilityId and | ||
x.size == y.size and | ||
x.requestId == y.requestId and | ||
x.slotIndex == y.slotIndex | ||
x.id == y.id | ||
proc `==`*(x, y: Availability): bool = | ||
x.id == y.id and | ||
x.size == y.size and | ||
x.duration == y.duration and | ||
x.maxCollateral == y.maxCollateral and | ||
x.minPrice == y.minPrice | ||
x.id == y.id |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a very vague recollection of just an id
comparison not being enough, but for the life of me, I can't remember why 😅 I guess we'll find out one way or the other 😂
tests/integration/codexclient.nim
Outdated
let url = client.baseurl & "/sales/availability/" & $availabilityId | ||
var json = %*{} | ||
|
||
if totalSize =? totalSize: | ||
json["totalSize"] = %totalSize | ||
|
||
if freeSize =? freeSize: | ||
json["freeSize"] = %freeSize | ||
|
||
if duration =? duration: | ||
json["duration"] = %duration | ||
|
||
if minPrice =? minPrice: | ||
json["minPrice"] = %minPrice | ||
|
||
if maxCollateral =? maxCollateral: | ||
json["maxCollateral"] = %maxCollateral | ||
|
||
client.http.patch(url, $json) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Serializing an optionalized type will output null
for none
fields, which are handled correctly in deserialization on the api side.
let url = client.baseurl & "/sales/availability/" & $availabilityId | |
var json = %*{} | |
if totalSize =? totalSize: | |
json["totalSize"] = %totalSize | |
if freeSize =? freeSize: | |
json["freeSize"] = %freeSize | |
if duration =? duration: | |
json["duration"] = %duration | |
if minPrice =? minPrice: | |
json["minPrice"] = %minPrice | |
if maxCollateral =? maxCollateral: | |
json["maxCollateral"] = %maxCollateral | |
client.http.patch(url, $json) | |
type OptRestAvailability = Optionalize(RestAvailability) | |
let availability = OptRestAvailability(totalSize: totalSize, freeSize: freeSize, duration: duration, minPrice: minPrice, maxCollateral: maxCollateral) | |
client.http.patch(url, availability.toJson) |
Another thought: we could also consider exporting this type from rest/json
instead of creating it each time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was banging my head on why integration tests were not passing and it seemed like there were no changes updated on the node's side, while when I tested it locally it worked. It turned out that yours suggestion is actually serializing into empty JSON object {}
😳
It is because the Optionalize
does not preserve the serialize
pragmas. Honestly, I don't want to tinker with the macro anymore as that would be another time blackhole, so I will reject your proposal, even though I like it more than the original.
If/when we find the need to extend the Optionalize
to honor serialize
pragmas then we can revisit this part.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No problem, I understand. For future reference, you can also annotate the type with {.serialize.}
and then there's no need to annotate each property, ie all properties will be serialized by default.
test "updating non-existing availability": | ||
let availability = client1.postAvailability(totalSize=140000.u256, duration=200.u256, minPrice=300.u256, maxCollateral=300.u256).get | ||
|
||
client1.patchAvailability(availability.id, duration=100.u256.some, minPrice=200.u256.some, maxCollateral=200.u256.some) | ||
|
||
let updatedAvailability = (client1.getAvailabilities().get).findItem(availability).get | ||
check updatedAvailability.duration == 100 | ||
check updatedAvailability.minPrice == 200 | ||
check updatedAvailability.maxCollateral == 200 | ||
check updatedAvailability.totalSize == 140000 | ||
check updatedAvailability.freeSize == 140000 | ||
|
||
let nonExistingResponse = client1.patchAvailabilityRaw(AvailabilityId.example, duration=100.u256.some, minPrice=200.u256.some, maxCollateral=200.u256.some) | ||
check nonExistingResponse.status == "404 Not Found" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Imo, all of these Availability integration tests should probably be handled by REST unit tests (which don't exist yet), instead of an integration test. Definitely out of scope of this PR obviously, but something we should look to implement in the future
82b5439
to
f928b2b
Compare
let getResult = await self.get(key, Availability) | ||
|
||
if getResult.isOk: | ||
let oldAvailability = !getResult | ||
|
||
# Sizing of the availability changed, we need to adjust the repo reservation accordingly | ||
if oldAvailability.totalSize != obj.totalSize: | ||
if oldAvailability.totalSize < obj.totalSize: # storage added | ||
if reserveErr =? (await self.repo.reserve((obj.totalSize - oldAvailability.totalSize).truncate(uint))).errorOption: | ||
return failure(reserveErr.toErr(ReserveFailedError)) | ||
|
||
elif oldAvailability.totalSize > obj.totalSize: # storage removed | ||
if reserveErr =? (await self.repo.release((oldAvailability.totalSize - obj.totalSize).truncate(uint))).errorOption: | ||
return failure(reserveErr.toErr(ReleaseFailedError)) | ||
else: | ||
let err = getResult.error() | ||
if not (err of NotExistsError): | ||
return failure(err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know this is not the nicest construct, but I have not figured a way how to achieve this using some nice construct from Questionable, because I need to continue the execution of the proc in case when the error is NotExistsError
and in the same time not to evaluate the sizing changes...
If you know of better way please let me know.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
without availability =? await self.get, error:
# do something with error
# do something with availability
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm, I know about this construct, but did not think of a way how I could wrap it on my use case. But you (again 😅) gave me idea :-D Thanks! Will fix it in follow-up PR.
Oh shi*, I have no idea how did I add this to the merge queue 😳 Please review it anyhow and if there are some outstanding issues, I will tackle them in follow up PR. |
let tSym = genSym(nskType, "T") | ||
nnkStmtList.newTree( | ||
nnkTypeSection.newTree( | ||
nnkTypeDef.newTree(tSym, newEmptyNode(), nnkObjectTy.newTree(newEmptyNode(), newEmptyNode(), fields)) | ||
), | ||
tSym | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+proc addSerializePragma(typeNode: NimNode): NimNode =
+ nnkPragmaExpr.newTree(
+ typeNode,
+ nnkPragma.newTree(
+ newIdentNode("serialize")
+ )
+ )
+ let tSymAnnotated = tSym.addSerializePragma()
let tree = nnkStmtList.newTree(
nnkTypeSection.newTree(
- nnkTypeDef.newTree(tSym, newEmptyNode(), nnkObjectTy.newTree(newEmptyNode(), newEmptyNode(), fields))
+ nnkTypeDef.newTree(tSymAnnotated, newEmptyNode(), nnkObjectTy.newTree(newEmptyNode(), newEmptyNode(), fields))
),
tSym
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! Did you test it? Does it work? Would you be willing to create PR for it?
IMHO I would just not be adding the pragma automatically to all the fields. Either, it should be smart enough to detect that the pragma was present on the original field and only then add it to the new type, or a simpler approach could be adding some flag to the Optionalize()
which would enable this behavior.
I split the work on this PR from the work on #467. It is a prerequisite for implementing persistent availabilities as there will need to be a way how to update and terminate them.
This PR brings several changes and breaking changes to the API endpoints:
POST /sales/availability
successful response type to201
Availability
structure where thesize
property was replaced bytotalSize
property which indicates the total capacity of the availability andfreeSize
which indicate the capacity of the availability that is available for sellingPATCH /sales/availability/{id}
that allows updating the availability properties except thefreeSize
. The updated properties are then used only for asserting the new incoming requests. The currently running requests are not effected by the update availabilityGET /sales/availability/{id}/reservations
which returns the list of Reservations that are linked to the Availability.Other notable addition is the
Optionalize()
macro which allows you to convertobject
type intoobject
type with its properties converted intoOption[T]
.Lastly, I have spent quite some time originally debugging an issue, where originally the
PATCH
endpoint was returning204 No Content
successful reply, but there seems to be some bug in either ofnim-presto
,chronos
server implementation orHTTPClient
which I have summarized in reproduction repository here and opted instead to use simple200
response instead.