diff --git a/honu.go b/honu.go index a2ff2f0..ee76e53 100644 --- a/honu.go +++ b/honu.go @@ -17,6 +17,7 @@ import ( pb "github.com/rotationalio/honu/object" opts "github.com/rotationalio/honu/options" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" ) // DB is a Honu embedded database. @@ -274,6 +275,7 @@ func (db *DB) Put(key, value []byte, options ...opts.Option) (_ *pb.Object, err obj = &pb.Object{ Key: key, Namespace: cfg.Namespace, + Created: timestamppb.Now(), } } else { return nil, err diff --git a/honu_test.go b/honu_test.go index ce74b3e..e47295a 100644 --- a/honu_test.go +++ b/honu_test.go @@ -89,6 +89,8 @@ func TestLevelDBInteractions(t *testing.T) { require.NoError(t, err) require.Equal(t, uint64(1), obj.Version.Version) require.False(t, obj.Tombstone()) + require.NotEmpty(t, obj.Created) + require.False(t, obj.Created.AsTime().IsZero()) // Delete the version from the database and ensure you // are not able to get the deleted version diff --git a/object/object.pb.go b/object/object.pb.go index 3acd23d..67b82f7 100644 --- a/object/object.pb.go +++ b/object/object.pb.go @@ -9,6 +9,7 @@ package object import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" ) @@ -45,6 +46,8 @@ type Object struct { Owner string `protobuf:"bytes,5,opt,name=owner,proto3" json:"owner,omitempty"` // The replica that created the object (identified as "pid:name" if name exists) // The object data that is only populated on Updates. Data []byte `protobuf:"bytes,10,opt,name=data,proto3" json:"data,omitempty"` + // The timestamp that the object was created (modified timestamps are on versions). + Created *timestamppb.Timestamp `protobuf:"bytes,15,opt,name=created,proto3" json:"created,omitempty"` } func (x *Object) Reset() { @@ -121,6 +124,13 @@ func (x *Object) GetData() []byte { return nil } +func (x *Object) GetCreated() *timestamppb.Timestamp { + if x != nil { + return x.Created + } + return nil +} + // Implements a geo-distributed version as a Lamport Scalar type Version struct { state protoimpl.MessageState @@ -132,6 +142,8 @@ type Version struct { Region string `protobuf:"bytes,3,opt,name=region,proto3" json:"region,omitempty"` // The region where the change occurred to track multi-region handling. Parent *Version `protobuf:"bytes,4,opt,name=parent,proto3" json:"parent,omitempty"` // In order to get a complete version history, identify the predessor; for compact data transfer parent should not be defined in parent version. Tombstone bool `protobuf:"varint,5,opt,name=tombstone,proto3" json:"tombstone,omitempty"` // Set to true in order to mark the object as deleted + // The timestamp that the version was created (e.g. the last modified date). + Modified *timestamppb.Timestamp `protobuf:"bytes,15,opt,name=modified,proto3" json:"modified,omitempty"` } func (x *Version) Reset() { @@ -201,36 +213,52 @@ func (x *Version) GetTombstone() bool { return false } +func (x *Version) GetModified() *timestamppb.Timestamp { + if x != nil { + return x.Modified + } + return nil +} + var File_object_v1_object_proto protoreflect.FileDescriptor var file_object_v1_object_proto_rawDesc = []byte{ 0x0a, 0x16, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x76, 0x31, 0x2f, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0e, 0x68, 0x6f, 0x6e, 0x75, 0x2e, 0x6f, - 0x62, 0x6a, 0x65, 0x63, 0x74, 0x2e, 0x76, 0x31, 0x22, 0xad, 0x01, 0x0a, 0x06, 0x4f, 0x62, 0x6a, - 0x65, 0x63, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, - 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x12, 0x31, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x68, 0x6f, 0x6e, 0x75, 0x2e, 0x6f, 0x62, 0x6a, 0x65, - 0x63, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x76, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x12, 0x14, - 0x0a, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6f, - 0x77, 0x6e, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x0a, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x9c, 0x01, 0x0a, 0x07, 0x56, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x70, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x04, 0x52, 0x03, 0x70, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, - 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x06, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x12, 0x2f, 0x0a, 0x06, 0x70, 0x61, 0x72, 0x65, - 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x68, 0x6f, 0x6e, 0x75, 0x2e, - 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x52, 0x06, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x6f, 0x6d, - 0x62, 0x73, 0x74, 0x6f, 0x6e, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x74, 0x6f, - 0x6d, 0x62, 0x73, 0x74, 0x6f, 0x6e, 0x65, 0x42, 0x25, 0x5a, 0x23, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x72, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, - 0x69, 0x6f, 0x2f, 0x68, 0x6f, 0x6e, 0x75, 0x2f, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xe3, 0x01, 0x0a, 0x06, 0x4f, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x12, 0x31, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x68, 0x6f, 0x6e, 0x75, 0x2e, 0x6f, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x07, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x67, 0x69, 0x6f, + 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x12, + 0x14, 0x0a, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x34, 0x0a, 0x07, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x22, + 0xd4, 0x01, 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x70, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x70, 0x69, 0x64, 0x12, 0x18, 0x0a, + 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x67, 0x69, 0x6f, + 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x12, + 0x2f, 0x0a, 0x06, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x17, 0x2e, 0x68, 0x6f, 0x6e, 0x75, 0x2e, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x2e, 0x76, 0x31, + 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, + 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x6f, 0x6d, 0x62, 0x73, 0x74, 0x6f, 0x6e, 0x65, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x09, 0x74, 0x6f, 0x6d, 0x62, 0x73, 0x74, 0x6f, 0x6e, 0x65, 0x12, 0x36, + 0x0a, 0x08, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x08, 0x6d, 0x6f, + 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x42, 0x25, 0x5a, 0x23, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x72, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x69, + 0x6f, 0x2f, 0x68, 0x6f, 0x6e, 0x75, 0x2f, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -247,17 +275,20 @@ func file_object_v1_object_proto_rawDescGZIP() []byte { var file_object_v1_object_proto_msgTypes = make([]protoimpl.MessageInfo, 2) var file_object_v1_object_proto_goTypes = []interface{}{ - (*Object)(nil), // 0: honu.object.v1.Object - (*Version)(nil), // 1: honu.object.v1.Version + (*Object)(nil), // 0: honu.object.v1.Object + (*Version)(nil), // 1: honu.object.v1.Version + (*timestamppb.Timestamp)(nil), // 2: google.protobuf.Timestamp } var file_object_v1_object_proto_depIdxs = []int32{ 1, // 0: honu.object.v1.Object.version:type_name -> honu.object.v1.Version - 1, // 1: honu.object.v1.Version.parent:type_name -> honu.object.v1.Version - 2, // [2:2] is the sub-list for method output_type - 2, // [2:2] is the sub-list for method input_type - 2, // [2:2] is the sub-list for extension type_name - 2, // [2:2] is the sub-list for extension extendee - 0, // [0:2] is the sub-list for field type_name + 2, // 1: honu.object.v1.Object.created:type_name -> google.protobuf.Timestamp + 1, // 2: honu.object.v1.Version.parent:type_name -> honu.object.v1.Version + 2, // 3: honu.object.v1.Version.modified:type_name -> google.protobuf.Timestamp + 4, // [4:4] is the sub-list for method output_type + 4, // [4:4] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name } func init() { file_object_v1_object_proto_init() } diff --git a/proto/buf.yaml b/proto/buf.yaml index b5a6b11..c72c02c 100644 --- a/proto/buf.yaml +++ b/proto/buf.yaml @@ -1,7 +1,5 @@ version: v1 name: buf.build/rotational/honudb -deps: - - buf.build/googleapis/googleapis breaking: use: - FILE diff --git a/proto/object/v1/object.proto b/proto/object/v1/object.proto index 474d26b..b09777b 100644 --- a/proto/object/v1/object.proto +++ b/proto/object/v1/object.proto @@ -1,6 +1,9 @@ syntax = "proto3"; package honu.object.v1; + +import "google/protobuf/timestamp.proto"; + option go_package = "github.com/rotationalio/honu/object"; // An Object is a generic representation of a replicated piece of data. At the top level @@ -16,22 +19,28 @@ option go_package = "github.com/rotationalio/honu/object"; // Additionally, it is recommended that a history table be maintained locally so that // Object versions can be rolled back to previous states where necessary. message Object { - // The object metadata that must be populated on both VersionVectors and Updates - bytes key = 1; // A unique key/id that represents the object across the namespace of the object type - string namespace = 2; // The namespace of the object, used to disambiguate keys or different object types - Version version = 3; // A version vector representing this objects current or latest version - string region = 4; // The region code where the data originated - string owner = 5; // The replica that created the object (identified as "pid:name" if name exists) - - // The object data that is only populated on Updates. - bytes data = 10; + // The object metadata that must be populated on both VersionVectors and Updates + bytes key = 1; // A unique key/id that represents the object across the namespace of the object type + string namespace = 2; // The namespace of the object, used to disambiguate keys or different object types + Version version = 3; // A version vector representing this objects current or latest version + string region = 4; // The region code where the data originated + string owner = 5; // The replica that created the object (identified as "pid:name" if name exists) + + // The object data that is only populated on Updates. + bytes data = 10; + + // The timestamp that the object was created (modified timestamps are on versions). + google.protobuf.Timestamp created = 15; } // Implements a geo-distributed version as a Lamport Scalar message Version { - uint64 pid = 1; // Process ID - used to deconflict ties in the version number. - uint64 version = 2; // Montonically increasing version number. - string region = 3; // The region where the change occurred to track multi-region handling. - Version parent = 4; // In order to get a complete version history, identify the predessor; for compact data transfer parent should not be defined in parent version. - bool tombstone = 5; // Set to true in order to mark the object as deleted -} \ No newline at end of file + uint64 pid = 1; // Process ID - used to deconflict ties in the version number. + uint64 version = 2; // Montonically increasing version number. + string region = 3; // The region where the change occurred to track multi-region handling. + Version parent = 4; // In order to get a complete version history, identify the predessor; for compact data transfer parent should not be defined in parent version. + bool tombstone = 5; // Set to true in order to mark the object as deleted + + // The timestamp that the version was created (e.g. the last modified date). + google.protobuf.Timestamp modified = 15; +} diff --git a/versions.go b/versions.go index ddf58a5..178ab4e 100644 --- a/versions.go +++ b/versions.go @@ -6,6 +6,7 @@ import ( "github.com/rotationalio/honu/config" pb "github.com/rotationalio/honu/object" + "google.golang.org/protobuf/types/known/timestamppb" ) // NewVersionManager creates a new manager for handling lamport scalar versions. @@ -83,10 +84,11 @@ func (v VersionManager) Delete(meta *pb.Object) error { return nil } -//Assigns the attributes of the passed versionManager to the object. +// Assigns the attributes of the passed versionManager to the object. func (v VersionManager) updateVersion(meta *pb.Object, delete_version bool) { meta.Version.Pid = v.PID meta.Version.Version++ meta.Version.Region = v.Region meta.Version.Tombstone = delete_version + meta.Version.Modified = timestamppb.Now() } diff --git a/versions_test.go b/versions_test.go index 19568ef..72bd4db 100644 --- a/versions_test.go +++ b/versions_test.go @@ -56,6 +56,8 @@ func TestVersionManager(t *testing.T) { require.Equal(t, vers1.Region, obj.Version.Region) require.Empty(t, obj.Version.Parent) require.False(t, obj.Tombstone()) + require.NotEmpty(t, obj.Version.Modified) + require.False(t, obj.Version.Modified.AsTime().IsZero()) // Create a new remote versioner conf.PID = 13 @@ -96,6 +98,8 @@ func TestVersionManager(t *testing.T) { require.Equal(t, uint64(1), obj.Version.Parent.Version) require.Equal(t, vers1.Region, obj.Version.Parent.Region) require.False(t, obj.Tombstone()) + require.NotEmpty(t, obj.Version.Modified) + require.False(t, obj.Version.Modified.AsTime().IsZero()) // Test Delete - creating a tombstone require.NoError(t, vers1.Delete(obj)) @@ -130,6 +134,8 @@ func TestVersionManager(t *testing.T) { require.Equal(t, uint64(2), obj.Version.Parent.Version) require.Equal(t, vers2.Region, obj.Version.Parent.Region) require.True(t, obj.Tombstone()) + require.NotEmpty(t, obj.Version.Modified) + require.False(t, obj.Version.Modified.AsTime().IsZero()) // Cannot delete a deleted object require.Error(t, vers1.Delete(obj)) @@ -174,4 +180,6 @@ func TestVersionManager(t *testing.T) { require.Equal(t, uint64(3), obj.Version.Parent.Version) require.Equal(t, vers1.Region, obj.Version.Parent.Region) require.False(t, obj.Tombstone()) + require.NotEmpty(t, obj.Version.Modified) + require.False(t, obj.Version.Modified.AsTime().IsZero()) }