-
Notifications
You must be signed in to change notification settings - Fork 65
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
Local first optimizations #450
Conversation
Signed-off-by: Carson Farmer <carson.farmer@gmail.com>
Signed-off-by: Carson Farmer <carson.farmer@gmail.com>
Signed-off-by: Carson Farmer <carson.farmer@gmail.com>
Signed-off-by: Carson Farmer <carson.farmer@gmail.com>
Signed-off-by: Carson Farmer <carson.farmer@gmail.com>
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.
Some comments for when this thing is no longer WIP.
id, err := getInstanceID(next) | ||
if err != nil { | ||
return nil, err | ||
} | ||
key := baseKey.ChildString(t.collection.name).ChildString(id.String()) | ||
previous, err := t.collection.db.datastore.Get(key) | ||
if err == ds.ErrNotFound { | ||
return nil, errCantSaveNonExistentInstance | ||
a := core.Action{ |
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.
This means we can "Save" an instance even if we haven't seem it before, so long as it already has a valid _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.
This is useful for reasons I've outlined elsewhere (see #440), but additionally, it circumvents a restriction on current Hub. With that in mind, we should think through the implications of this a little bit. Seems safe on the outset.
@@ -656,11 +739,11 @@ func getSchemaTypeProperties(jt *jsonschema.Type, defs jsonschema.Definitions) ( | |||
if jt.Ref != "" { | |||
parts := strings.Split(jt.Ref, "/") | |||
if len(parts) < 1 { | |||
return nil, ErrInvalidCollectionSchema | |||
return make(map[string]*jsonschema.Type), nil |
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.
This should still be safe, because we always check and add the _id as needed. So even if the schema doesn't call for it, all ThreadDB instances will get an _id field. This makes it easier to support "empty" schemas.
@@ -689,6 +772,28 @@ func setNewInstanceID(t []byte) (core.InstanceID, []byte) { | |||
return newID, patchedValue | |||
} | |||
|
|||
func getModifiedTag(t []byte) (int64, error) { |
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 a copy-pasta pretty much from the _id equivalent.
@@ -20,12 +21,14 @@ const ( | |||
|
|||
type Person struct { | |||
ID core.InstanceID `json:"_id"` | |||
Mod int64 `json:"_mod"` |
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.
Added for tests, this isn't needed by end-users. They can leave this off and things will work just fine.
@@ -959,8 +1070,83 @@ func TestSaveInstance(t *testing.T) { | |||
checkErr(t, err) | |||
|
|||
p := util.JSONFromInstance(Person{Name: "Alice", Age: 42}) | |||
if err := m.Save(p); !errors.Is(err, errCantSaveNonExistentInstance) { | |||
t.Fatal("shouldn't save non-existent instasnce") | |||
if err := m.Save(p); err != nil { |
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.
Adjusting to allow save without prior instance.
@@ -60,7 +61,7 @@ var ( | |||
func init() { | |||
nameRx = regexp.MustCompile(`^[A-Za-z0-9]+(?:[-][A-Za-z0-9]+)*$`) | |||
// register empty map in order to gob-encode old-format events with non-encodable time.Time which cbor decodes as map[string]interface{} | |||
gob.Register(map[string]interface {}{}) | |||
gob.Register(map[string]interface{}{}) |
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.
linter I guess?
@@ -390,7 +392,8 @@ func runListenersComplexUseCase(t *testing.T, los ...ListenOption) []Action { | |||
// Collection2 Save j1 | |||
j1 = util.SetJSONProperty("Counter", -1, j1) | |||
j1 = util.SetJSONProperty("Name", "Textile33", j1) | |||
checkErr(t, c2.Save(j1)) | |||
err = c2.Save(j1) |
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.
These are leftover from some earlier changes. I can revert them if we want.
@@ -43,10 +43,14 @@ func newDispatcher(store datastore.TxnDatastore) *dispatcher { | |||
} | |||
|
|||
// Store returns the internal event store. | |||
func (d *dispatcher) Store() datastore.Datastore { | |||
func (d *dispatcher) Store() datastore.TxnDatastore { |
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.
Always has been a txndatastore in practice, now I want to make this explicit.
return d.store | ||
} | ||
|
||
func (d *dispatcher) Lock() *sync.RWMutex { |
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 handy
@@ -122,7 +126,8 @@ func getKey(event core.Event) (key datastore.Key, err error) { | |||
} | |||
time := strconv.FormatInt(unix, 10) | |||
key = dsDispatcherPrefix.ChildString(time). | |||
ChildString(event.InstanceID().String()). | |||
ChildString(event.Collection()) | |||
ChildString(event.Collection()). |
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.
Up for discussion, but it defs makes queries for specific collection types within a db easier, and a lot more efficient.
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.
As far as I know, we aren't touching this data from anywhere else, so this shouldn't require a migration or check anywhere.
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, good call
} | ||
|
||
// ModifiedSince returns a list of all instances that have been modified (and/or touched) since `time`. | ||
func (c *Collection) ModifiedSince(time int64, opts ...TxnOption) (modified []core.InstanceID, err error) { |
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.
Note that I have not yet exposed this via any gRPC APIs/updates. Partly because I can work around having this for now using less efficient means, and partly because it should be reviewed before I bother exposing it.
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.
Would it make sense to combine this with Find
, possibly as a new kind of query / condition? Then you could combine this functionality with other queries / conditions.
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, interesting. That would be ideal (no new APIs). I'm not sure off the top of my head how this might work given we are querying the dispatcher directly... but as you suggest, some sort of additional query condition/operator might do the trick. Will look into this. In that case, I might mark that API as experimental, not expose it via gRPC for now, and explore this alternative in a follow on PR. Seem reasonable?
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.
SGTM!
id, err := getInstanceID(next) | ||
if err != nil { | ||
return nil, err | ||
} | ||
key := baseKey.ChildString(t.collection.name).ChildString(id.String()) | ||
previous, err := t.collection.db.datastore.Get(key) | ||
if err == ds.ErrNotFound { | ||
return nil, errCantSaveNonExistentInstance | ||
a := core.Action{ |
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.
This is useful for reasons I've outlined elsewhere (see #440), but additionally, it circumvents a restriction on current Hub. With that in mind, we should think through the implications of this a little bit. Seems safe on the outset.
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, LGTM 💪
@@ -122,7 +126,8 @@ func getKey(event core.Event) (key datastore.Key, err error) { | |||
} | |||
time := strconv.FormatInt(unix, 10) | |||
key = dsDispatcherPrefix.ChildString(time). | |||
ChildString(event.InstanceID().String()). | |||
ChildString(event.Collection()) | |||
ChildString(event.Collection()). |
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, good call
} | ||
|
||
// ModifiedSince returns a list of all instances that have been modified (and/or touched) since `time`. | ||
func (c *Collection) ModifiedSince(time int64, opts ...TxnOption) (modified []core.InstanceID, err error) { |
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.
Would it make sense to combine this with Find
, possibly as a new kind of query / condition? Then you could combine this functionality with other queries / conditions.
This PR contains a few small nits and optimizations that make building on offline-first client for ThreadDB easier. The main changes are that a call to Save can now also lead to a Create action being generated if and only if the call to Save includes an existing Instance ID as a simple safety check. This allows callers to write or overwrite an existing Instance, without having to check first if it already exists. An additional change is that Delete now silently ignores missing Instances. This means a client can call delete on an Instance that might have already been deleted while it was offline or similar, without throwing an error.
Finally, the more important addition is the "special" _mod field. This is simple a "readonly" timestamp that gets added to all Instances. Any changes to this field will be ignored by the peer, and will be automatically updated on Save (and initialized on Create). This doesn't really have any effect on Delete, which is partly why the ModifiedSince experimental API was also created.