-
Notifications
You must be signed in to change notification settings - Fork 1.6k
[De]serialize Timestamps via the remote Serializer #1233
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
Conversation
@@ -216,6 +217,27 @@ struct hash<firebase::Timestamp> { | |||
// implementation is subject to change. | |||
size_t operator()(const firebase::Timestamp& timestamp) const; | |||
}; | |||
|
|||
template <> | |||
class numeric_limits<firebase::Timestamp> { |
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.
So this is ... curious. :) Specializing std::numeric_limits with our types works and is an interesting way to surface this. But it's not clear to me if this is either (a) a good idea, or (b) even legal. (The spec's a little vague on this for non-standard types.) We could trivially move this to static Timestamp::Max()
or similar. If we choose to keep it here, we'd eventually want to fill it out a bit more.
Having this available also implies some of the Timestamp implementation and tests could take advantage of this too, but I haven't adjusted that in this PR. (It could be a followup.)
Another alternative might be to just redefine these values (again) in the serializer (but then we'd have 3 copies of it floating around.)
Yet another alternative would be to add a factory method that returns a StatusOr (which returns a bad status rather than asserting when values are too large) and use that rather than the ctor in the serializer.
Options tl;dr:
- specialize std::numeric_limits
- Timestamp::Max()/Min()
- Copy kUpperBound to serializer.cc (and don't touch Timestamp)
- static StatusOr Timestamp::Create(seconds, nanos)
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 like having max
and min
. As for selecting between 1 and 2... On my reading, it also appears that specializing numeric_limits
is a little murky. It appears to be allowed. OTOH, it might be more trouble than it's worth:
- referring to the standard for guidance,
<chrono>
'stime_point
which is reasonably similar toTimestamp
doesn't specializenumeric_limits
. This is of course non-binding. - I don't think that requirements from the standard apply to specialization for a user-defined type, but not implementing them will make the specialization inconsistent with the standard, and implementing them is a lot of boilerplate (defining other member functions, providing const/non-const specializations).
- just in general, it appears that specializing
numeric_limits
for user-defined types is underspecified and probably not expected to be a common occurrence (unlike e.g.hash
).
So all in all, I'd vote for option 2. For its pros and cons:
- since the notion of maximum and minimum time is pretty explicit in
Timestamp
, I think it's reasonable to encode that; - OTOH, I'm not sure if this change would require a round of review (the only way around that would be option 3, though).
For option 4, I'm wary because this will entail making Firebase common depend on StatusOr
, which might be undesirable. We could, however, define this function as a helper just within Firestore.
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.
Just to brainstorm:
- there could also be a function (member or free) like
IsWithinSupportedRange
(although it would make it harder to report the error precisely). - these could be public constants (e.g.,
kMaxSeconds
).
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.
For option 4, I'm wary because this will entail making Firebase common depend on StatusOr, which might be undesirable. We could, however, define this function as a helper just within Firestore.
Good point. We could migrate firebase::firestore::Status to firebase::Status... but I'm not sure I want to take that on.
OTOH, I'm not sure if this change would require a round of review (the only way around that would be option 3, though).
Yeah, I was a little concerned about that too. However, this issue might only exist in c++? For instance, in java, anyone who cares about the range can just try it and "ask for forgiveness rather than permission". IOW, they can catch the resulting exception. (And since all of the c++ sdk needs to eventually get reviewed...) But I'm pointing this out from more of a theoretical viewpoint; if we're going to expose a max/min, then I'd like to do so across platforms.
Anyways, since the rest of the issues so far are nits, I'm going to drag @wilhuff in now to get his opinion too.
(I'm leaning toward #2 too.)
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.
FWIW, my general take is that unless something is described as the way to do something (and there's lots of prior art) it's best to stay away from extending the standard library. The essential problem is that it puts us in a position where things can fail for obscure reasons on compilers/libraries we don't use often for very little benefit.
If someone really, really needed a specialization of numeric_limits
it's trivial to implement given Min
and Max
functions we supply, but if our specialization of numeric_limits
interacts poorly with something else in a user system it's very difficult to work around.
Secondarily, and more specific to this case: std::numeric_limits
is a poor fit for what we're doing here. Those traits include members like epsilon
, infinity
, and quiet_NaN
(http://en.cppreference.com/w/cpp/types/numeric_limits). std::numeric_limits
defines reasonable behavior for all of the specializations in the standard library and we can't reasonably do so for Timestamp
. This makes it unlikely that this specialization will work for any algorithm that might consume a type based on std::numeric_limits
traits.
I'm also not a fan of (2) because it's a committed API for which we don't necessarily know there's a demonstrable need.
My proposal is a variation on (3) (call it (5)):
Put internal helpers for a type that aren't part of the committed API in a separate header, e.g. right next to timestamp.cc
add timestamp_internal.h
that includes whatever you need to share between timestamp.cc
and the serializer. Be mindful of how this is shared on the other platforms (if at all).
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.
Secondarily, and more specific to this case: std::numeric_limits is a poor fit for what we're doing here. Those traits include members like epsilon, infinity, and quiet_NaN
Note that this also applies to things like int. The standard handles this by simply not defining the parts that don't make sense[1]; we could do the same. But I otherwise agree; lets not do this. :)
I'm also not a fan of (2) because it's a committed API for which we don't necessarily know there's a demonstrable need.
One could imagine users wanting it for the same reason I do. If you are creating a timestamp based on info from an untrusted source (network/user/etc), then, because the ctor asserts, you'll want to verify it first. The best way to do so is to ask the object itself, since (hopefully) it will stay in sync with itself. But I'm fine deferring this.
My proposal is a variation on (3) (call it (5)):
Done. In the future, if we want to do (2), we can just merge these two files.
[1] - well, not quite. They're defined, but return nonsense. There's also things like numeric_limits::has_infinity
which you can query to determine if you'll get something reasonable. eg std::numeric_limits<int>::has_infinity
is false.
@@ -259,6 +270,23 @@ TEST_F(SerializerTest, EncodesString) { | |||
} | |||
} | |||
|
|||
TEST_F(SerializerTest, EncodesTimestamps) { | |||
std::vector<Timestamp> cases{ | |||
Timestamp(), // epoch |
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.
Optional nit: consider {}
instead of Timestamp()
for symmetry with the other cases.
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.
Done.
@@ -216,6 +217,27 @@ struct hash<firebase::Timestamp> { | |||
// implementation is subject to change. | |||
size_t operator()(const firebase::Timestamp& timestamp) const; | |||
}; | |||
|
|||
template <> | |||
class numeric_limits<firebase::Timestamp> { |
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 like having max
and min
. As for selecting between 1 and 2... On my reading, it also appears that specializing numeric_limits
is a little murky. It appears to be allowed. OTOH, it might be more trouble than it's worth:
- referring to the standard for guidance,
<chrono>
'stime_point
which is reasonably similar toTimestamp
doesn't specializenumeric_limits
. This is of course non-binding. - I don't think that requirements from the standard apply to specialization for a user-defined type, but not implementing them will make the specialization inconsistent with the standard, and implementing them is a lot of boilerplate (defining other member functions, providing const/non-const specializations).
- just in general, it appears that specializing
numeric_limits
for user-defined types is underspecified and probably not expected to be a common occurrence (unlike e.g.hash
).
So all in all, I'd vote for option 2. For its pros and cons:
- since the notion of maximum and minimum time is pretty explicit in
Timestamp
, I think it's reasonable to encode that; - OTOH, I'm not sure if this change would require a round of review (the only way around that would be option 3, though).
For option 4, I'm wary because this will entail making Firebase common depend on StatusOr
, which might be undesirable. We could, however, define this function as a helper just within Firestore.
/** | ||
* Writes a nanopb message to the output stream. | ||
* | ||
* This essentially wraps calls to nanopb's pb_encode() method. If we didn't |
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.
Ultranit: since you're using backticks further on in the comment, consider wrapping pb_encode() in backticks as well.
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.
Done
std::numeric_limits<Timestamp>::min().seconds()) { | ||
reader->set_status( | ||
Status(FirestoreErrorCode::DataLoss, | ||
"Input proto bytes cannot be parsed (timestamp too small)")); |
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.
Optional: consider slightly rephrasing "timestamp too small" to something like "timestamp beyond the earliest supported date" (similarly for "too large").
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.
Done.
@@ -216,6 +217,27 @@ struct hash<firebase::Timestamp> { | |||
// implementation is subject to change. | |||
size_t operator()(const firebase::Timestamp& timestamp) const; | |||
}; | |||
|
|||
template <> | |||
class numeric_limits<firebase::Timestamp> { |
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.
Just to brainstorm:
- there could also be a function (member or free) like
IsWithinSupportedRange
(although it would make it harder to report the error precisely). - these could be public constants (e.g.,
kMaxSeconds
).
|
||
/** | ||
* Details about the Timestamp class which are useful internally, but we don't | ||
* want to expose publicly. Currently, just a collection of limits. |
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.
Optional nit: I'd delete the "Currently" part, this looks like a comment that is prone to become stale.
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.
Done
public: | ||
/** | ||
* Represents the maximum allowable time that the Timestamp class | ||
* handles, |
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.
Nit: join this line and the next one?
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.
Done.
|
||
#include "Firestore/core/include/firebase/firestore/timestamp.h" | ||
|
||
namespace firebase { |
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'm not sure about the correct namespace here -- no strong opinion, just pointing it out.
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'm not sure either. I put it in firebase since (a) that's where the other timestamp is and (b) under the assumption that the rest of firebase will want to use this for the same purpose (or at least, the rest of firebase that uses timestamp.)
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.
LGTM
void EncodeTimestamp(Writer* writer, const Timestamp& timestamp_value) { | ||
google_protobuf_Timestamp timestamp_proto = | ||
google_protobuf_Timestamp_init_zero; | ||
timestamp_proto.seconds = timestamp_value.seconds(); |
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.
Instead of initializing to zero and then assigning something else, why not
google_protobuf_Timestamp timestamp_proto{
.seconds = timestamp_value.seconds(),
.nanos = timestamp_value.nanoseconds()
};
Actually it looks like designated initializers are a C99 feature not standardized in C++11. Humbug.
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.
A slight modification on what you wrote does work though:
google_protobuf_Timestamp timestamp_proto{
timestamp_value.seconds(),
timestamp_value.nanoseconds()
};
However, if new fields were added to this struct, c++ wouldn't necessarily catch it. With our compiler, we'd be ok, since we define -Werror=missing-field-initializers
, but I'm unsure about other compilers (eg msvc).
Left as is for now.
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 clang is the primary compiler driving our CI we'd get a broken build immediately if a field was added so I'm not really concerned about the number of arguments. The order of the arguments is what makes me think this would be a bad idea. I bet it would compile if you reversed the arguments :-(. Anyway, carry on.
// rather not abort in these situations. | ||
if (timestamp_proto.seconds < TimestampInternal::Min().seconds()) { | ||
reader->set_status(Status(FirestoreErrorCode::DataLoss, | ||
"Input proto bytes cannot be parsed (timestamp " |
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.
Nit: This error message is kind of tortured and it's a little weird that the most specific thing we know is parenthetical.
How about: "Invalid message: timestamp beyond the earliest supported date"
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.
Done (also two immediately below.)
No description provided.