Skip to content

Conversation

@rhino1998
Copy link
Contributor

@rhino1998 rhino1998 commented Sep 23, 2024

Summary

This adds a new Time type to be used in plugin specs that can represent either fixed times or relative times. A future PR should extend the duration parsing to handle days, etc.

kind: source
spec:
  name: "orb"
  registry: "grpc"
  path: "localhost:7777"
  version: "v1.0.0"
  tables:
    ["*"]
  destinations:
    - "sqlite"
  spec:
    api_key: "${CQ_ORB_API_KEY}"
    timeframe_start: 10 days ago
    timeframe_end: "2024-09-25T03:52:40.14157935-04:00"

Here timeframe_start and timeframe_end are both configtype.Time types. Using mixed relative/fixed timestamps for timeframes isn't really useful here as the relative one will be relative to the run-time of the sync but in this example having

timeframe_start: 10 days ago
timeframe_end: now

or even omitting timeframe_end entirely is more useful

timeframe_start: 10 days ago

The plugin is free to define default like so

type Spec struct {
	// TimeframeStart is the beginning of the timeframe in which to fetch cost data.
	TimeframeStart configtype.Time `json:"timeframe_start"`

	// TimeframeEnd is the beginning of the timeframe in which to fetch cost data.
	TimeframeEnd configtype.Time `json:"timeframe_end"`

	// ...
}


func (s *Spec) SetDefaults() {
	if s.TimeframeStart.IsZero() {
		s.TimeframeStart = configtype.NewRelativeTime(-1 * 365 * 24 * time.Hour)
	}

	if s.TimeframeEnd.IsZero() {
		s.TimeframeEnd = configtype.NewRelativeTime(0)
	}
}

@rhino1998 rhino1998 requested review from a team, hermanschaaf and marianogappa September 23, 2024 16:36
@github-actions github-actions bot added the feat label Sep 23, 2024
Copy link
Member

@erezrokah erezrokah left a comment

Choose a reason for hiding this comment

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

Hi @rhino1998, nice 🚀 Can you give an example how this will be used in a plugin and in a spec file?

@github-actions github-actions bot added feat and removed feat labels Sep 23, 2024
@github-actions github-actions bot added feat and removed feat labels Sep 23, 2024
@rhino1998
Copy link
Contributor Author

Added some more context. Should configtype.Time.Time take a time.Time instead of a func() time.Time?

@github-actions github-actions bot added feat and removed feat labels Sep 23, 2024
Copy link
Member

@erezrokah erezrokah left a comment

Choose a reason for hiding this comment

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

I like the idea of having this kind of type as an SDK helper.

A few comments:

  1. If the purpose is to make it easier to configure relative times, I would use https://github.com/tj/go-naturaldate/tree/master instead of a wrapper over duration
  2. I would hide the IsRelative and NewRelativeTime and do NewTime(t string) that accepts either an RFC3339 or a natural date string. I don't see a reason consumers need to know the internal representation, also I don't think we even need typ timeType, as you can have NewTime(t string, base time.Time) and convert the string on creation.

@hermanschaaf
Copy link
Member

hermanschaaf commented Sep 24, 2024

If the purpose is to make it easier to configure relative times, I would use https://github.com/tj/go-naturaldate/tree/master instead of a wrapper over duration

I would push back on this; the library was last updated 4 years ago, and supports much more than we need to support. I think we would be better off supporting a small, well-defined number of formats.

Also I think the purpose here is not to make it easier, it's to make it possible at all (in Cloud), in a standardized way.

@erezrokah
Copy link
Member

erezrokah commented Sep 24, 2024

the library was last updated 4 years ago

I agree it hasn't change in a long time, but it doesn't have any other dependencies (other than the ones used for testing), and the author is https://github.com/tj (https://x.com/tjholowaychuk), which has a bunch of other popular repositories like https://github.com/tj/commander.js and https://github.com/koajs/koa

well-defined number of formats.

Agree it does more than we need, but timeframe_start: -1000h seems more machine friendly than human friendly to me

Also I think the purpose here is not to make it easier, it's to make it possible at all (in Cloud), in a standardized way.

Can you add more context on the Cloud use case? We want the backend to specify a duration via spec generated via backend code in Go? If so is it possible to convert the duration to time before?

@hermanschaaf
Copy link
Member

Can you add more context on the Cloud use case? We want the backend to specify a duration via spec generated via backend code in Go? If so is it possible to convert the duration to time before?

We want users to be able to specify a relative time, such as "30 days ago", which will be stored as part of the spec. Every time the sync runs, this should be converted to a timestamp relative to the current time. This way, we don't need to involve the backend to do string interpolation on the spec, and things remain relatively simple.

Right now, what happens in many plugins is they allow only a fixed timestamp to be specified, such as 2024-05-01T00:00:00Z, or a default value, often relative (30 days ago or now). This doesn't lend us the flexibility we desire for scheduled syncs in Cloud

@hermanschaaf
Copy link
Member

Agree it does more than we need, but timeframe_start: -1000h seems more machine friendly than human friendly to me

I agree with this.

I'm also okay with using that library, but if we do, let's not support everything that the library supports, but instead limit it to a defined list of supported formats that we can control.

I would maybe say something like:

  • now
  • x seconds [ago|from now]
  • x minutes [ago|from now]
  • x hours [ago|from now]
  • x days [ago|from now]

would be a good enough start (we can always add more if requested)

@erezrokah
Copy link
Member

I would maybe say something like:

  • now
  • x seconds [ago|from now]
  • x minutes [ago|from now]
  • x hours [ago|from now]
  • x days [ago|from now]

would be a good enough start (we can always add more if requested)

Sounds like a plan to me

@hermanschaaf
Copy link
Member

I forgot that in addition to relative times, we'd also want to support fixed timestamps of course, probably using RFC 3339. And also some support for plain dates, e.g. 2024-05-10, for plugins that need it.

{
Name: "complex relative duration",
Err: false,
Spec: `"10 months 3 days 4h20m from now"`,
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 if we're going to expose MarshalJSON then we should probably make this format 10 months 3 days 4 hours 20 minutes from now for consistency (and so we don't need to make a breaking change later to fix it)

Copy link
Member

Choose a reason for hiding this comment

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

Alternatively we could remove the ability to specify an arbitrary duration in the constructor (just remove the constructor) and only allow this to be constructed using a simple string like 10 days ago

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The reason for the weird hybrid format is for backwards compat for the Duration type. It ensures that anything constructed from a plain duration will emit the same json as before this PR. It may be reasonable to separate the json serialization format and the string parsed format and make the serialization format (but still unmarshalling the nicer human format) just [-+](\d+[a-zA-Z])+ (basically time.ParseDuration format but with additional units). That frees us to have the string format be nicer and human readable

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member

@erezrokah erezrokah left a comment

Choose a reason for hiding this comment

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

Thanks for the following @rhino1998, added a few more comments

"github.com/invopop/jsonschema"
)

var (
Copy link
Member

Choose a reason for hiding this comment

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

I prefer if we don't change the semantics of the existing duration type, as that could breaking existing behavior for consumers of this type

Copy link
Contributor Author

@rhino1998 rhino1998 Sep 25, 2024

Choose a reason for hiding this comment

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

All previously valid values are still valid for the duration type. This just adds more possible valid values. Additionally all previously valid values will serialize to the same string after this PR in its current state. (unless I missed something)

Copy link
Member

Choose a reason for hiding this comment

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

This just adds more possible valid values.

Exactly, so now the JSON validation is more relaxed, so something that would have failed before can pass now.
While this could be seen as a feature, it's better if plugins opt-in to it, then document the new behavior

}

func (t Time) MarshalJSON() ([]byte, error) {
switch t.typ {
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 we can keep the original string that we got in ParseTime and return that instead, WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

263750b, but unclear what the handling for an uninitialized Time or Duration should be. Currently it just marshals to ""

Copy link
Contributor Author

@rhino1998 rhino1998 Sep 26, 2024

Choose a reason for hiding this comment

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

Also, as a side-effect Time and Duration are no longer == comparable in a useful way

Copy link
Member

Choose a reason for hiding this comment

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

263750b, but unclear what the handling for an uninitialized Time or Duration should be. Currently it just marshals to ""

I think we need to decide that and pick whatever we want to happen, we can

  1. Error out on uninitialized
  2. Return MarshalJSON of the zero value of time.Time
  3. Return MarshalJSON of the zero value of time.Duration
  4. Return null
  5. Maybe more options

Not sure what's the best option but it needs to be consistent with UnmarshalJSON.
We can see what happens for time.Time and time.Duration zero values

Copy link
Member

Choose a reason for hiding this comment

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

Also, as a side-effect Time and Duration are no longer == comparable in a useful way

How could you compare Time and Duration before? Where do you need this comparison and how will it be used?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Duration was comparable with == (as it was just a struct) containing a time.Duration. I'm unsure if that was used anywhere but removing that is a breaking change. Adding days,months,etc to the struct maintained comparability for equal values but adding the input string field breaks that for anything besides values that had the same input string even if they're otherwise equivalent (including whitespace differences).

I guess Time wasn't usefully comparable with == as while time.Time is == comparable doing doesn't handle timezones and so is not recommended.

As for where it is needed, it isn't exactly for time, but the duration change is breaking change in the API (unclear if anything actually uses it though)

Copy link
Member

Choose a reason for hiding this comment

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

Ok so:

  1. I don't think we should modify the existing duration config type. Relaxing the parsing is also a breaking change
  2. Even if we did modify the existing type, we can used the parsed value of the duration for Equals

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Regarding 2., that still will have broken == comparison, although again unclear if that is used externally or if that even counts as an explicit part of the API to be broken

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Regarding default behavior, returning the zero value of time seems sane as it'll allow the current IsZero check to work. Although that is "0001-01-01T00:00:00Z"

https://go.dev/play/p/qI6SprLcw5p

rhino1998 and others added 4 commits September 25, 2024 14:43
Co-authored-by: Erez Rokah <erezrokah@users.noreply.github.com>
@hermanschaaf
Copy link
Member

@erezrokah @rhino1998 I lost track a bit, is there still anything unresolved on this PR?

@erezrokah
Copy link
Member

@erezrokah @rhino1998 I lost track a bit, is there still anything unresolved on this PR?

Not sure https://github.com/cloudquery/plugin-sdk/pull/1905/files#r1774764467 is resolved, also #1905 (comment)

I'd rather if we don't change the existing type behavior as that implicitly change spec parsing behavior for plugins. If plugins want to support the new human friend format mixed duration/time format they should do it explicitly I think

@rhino1998
Copy link
Contributor Author

@erezrokah @rhino1998 I lost track a bit, is there still anything unresolved on this PR?

Not sure https://github.com/cloudquery/plugin-sdk/pull/1905/files#r1774764467 is resolved, also #1905 (comment)

I'd rather if we don't change the existing type behavior as that implicitly change spec parsing behavior for plugins. If plugins want to support the new human friend format mixed duration/time format they should do it explicitly I think

The new mixed duration/time format is opt-in, but the new duration format does extend the old plain time.ParseDuration-based format.

How would you prefer the opt-in be done? A DurationV2 type? A separate package?

@erezrokah
Copy link
Member

erezrokah commented Sep 30, 2024

How would you prefer the opt-in be done? A DurationV2 type? A separate package?

I don't mind I mostly don't want to change the existing behavior of the current type. You can also put everything under the new Time type without a separate type

{
Name: "complex relative duration",
Err: false,
Spec: `"10 months 3 days 4h20m from now"`,
Copy link
Member

Choose a reason for hiding this comment

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

Should we change this to @hermanschaaf's suggestion of 10 months 3 days 4 hours 20 minutes from now?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In its current state, both are handled and whatever was input will be emitted. I added some test cases to make this clearer. 1a5ae0a

@erezrokah
Copy link
Member

Nothing blocking on my end, so you can merge at will. We might want to wait with releasing the new SDK until after today's plugins release cycle to avoid creating noise for the plugins teams

@disq
Copy link
Member

disq commented Oct 1, 2024

I think (sorry for the late comment) the from now syntax can be confusing, as people could expect 2 days from <some date here> or worse, 2 days from new year's eve to work.

@rhino1998
Copy link
Contributor Author

I think (sorry for the late comment) the from now syntax can be confusing, as people could expect 2 days from <some date here> or worse, 2 days from new year's eve to work.

We could pretty easily add support for stuff like 2 days from <timestamp>. That actually generalizes the input pretty nicely and will simplify/merge validation cases

@rhino1998
Copy link
Contributor Author

@disq Do you consider the above blocking?

@disq
Copy link
Member

disq commented Oct 3, 2024

@disq Do you consider the above blocking?

@rhino1998 No not at all.

@erezrokah
Copy link
Member

Not sure what's the status here, should we merge this or close it as stale?

@kodiakhq kodiakhq bot merged commit f57c3eb into main Oct 22, 2024
7 checks passed
@kodiakhq kodiakhq bot deleted the feat/configtype-time branch October 22, 2024 19:58
kodiakhq bot pushed a commit that referenced this pull request Oct 31, 2024
🤖 I have created a release *beep* *boop*
---


## [4.68.0](v4.67.1...v4.68.0) (2024-10-31)


### Features

* Add Time configtype ([#1905](#1905)) ([f57c3eb](f57c3eb))
* Support for quota query interval header ([#1948](#1948)) ([bfce6fe](bfce6fe))
* Test `MeterUsage` API call on initial setup of client ([#1906](#1906)) ([78df77d](78df77d))


### Bug Fixes

* Clean up usage retry logic ([#1950](#1950)) ([ca982f9](ca982f9))
* **deps:** Update module github.com/cloudquery/plugin-pb-go to v1.25.0 ([#1946](#1946)) ([b8e3e10](b8e3e10))

---
This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants