This is an automated email from the ASF dual-hosted git repository.

hanahmily pushed a commit to branch property-ttl
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git

commit 3fc9abf8bacf5414fcfb2e5eeb25c987bdede718
Author: Gao Hongtao <[email protected]>
AuthorDate: Sun Sep 17 11:43:54 2023 +0000

    Add Keepalive
    
    Signed-off-by: Gao Hongtao <[email protected]>
---
 CHANGES.md                                         |   1 +
 api/proto/banyandb/property/v1/property.proto      |   2 +-
 api/proto/banyandb/property/v1/rpc.proto           |  10 ++
 banyand/liaison/grpc/property.go                   |  12 ++-
 banyand/metadata/schema/property.go                | 107 ++++++++++++---------
 banyand/metadata/schema/schema.go                  |   3 +-
 bydbctl/internal/cmd/property.go                   |  23 ++++-
 bydbctl/internal/cmd/property_test.go              |  47 ++++++++-
 bydbctl/internal/cmd/rest.go                       |   1 +
 docs/api-reference.md                              |  29 ++++++
 docs/concept/data-model.md                         |  28 ++++++
 docs/crud/property.md                              |  39 ++++++++
 test/integration/standalone/other/property_test.go |  28 +++++-
 13 files changed, 279 insertions(+), 51 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index 58fcd220..6c5157f4 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -16,6 +16,7 @@ Release Notes.
 - Fix parse environment variables error
 - Implement the distributed query engine.
 - Add mod revision check to write requests.
+- Add TTL to the property.
 
 ### Bugs
 
diff --git a/api/proto/banyandb/property/v1/property.proto 
b/api/proto/banyandb/property/v1/property.proto
index 40aac5f6..ad124a4b 100644
--- a/api/proto/banyandb/property/v1/property.proto
+++ b/api/proto/banyandb/property/v1/property.proto
@@ -43,7 +43,7 @@ message Property {
   repeated model.v1.Tag tags = 2 [(validate.rules).repeated.min_items = 1];
   // updated_at indicates when the property is updated
   google.protobuf.Timestamp updated_at = 3;
-   // readonly. lease_id is the ID of the lease that attached to key.
+  // readonly. lease_id is the ID of the lease that attached to key.
   int64 lease_id = 4;
   // ttl indicates the time to live of the property.
   // It's a string in the format of "1h", "2m", "3s", "1500ms".
diff --git a/api/proto/banyandb/property/v1/rpc.proto 
b/api/proto/banyandb/property/v1/rpc.proto
index c00a7686..a7955fd7 100644
--- a/api/proto/banyandb/property/v1/rpc.proto
+++ b/api/proto/banyandb/property/v1/rpc.proto
@@ -45,6 +45,7 @@ message ApplyResponse {
   // True: the property is absent. False: the property existed.
   bool created = 1;
   uint32 tags_num = 2;
+  int64 lease_id = 3;
 }
 
 message DeleteRequest {
@@ -76,6 +77,12 @@ message ListResponse {
   repeated banyandb.property.v1.Property property = 1;
 }
 
+message KeepAliveRequest {
+  int64 lease_id = 1;
+}
+
+message KeepAliveResponse {}
+
 service PropertyService {
   // Apply creates a property if it's absent, or update a existed one based on 
a strategy.
   rpc Apply(ApplyRequest) returns (ApplyResponse) {
@@ -99,4 +106,7 @@ service PropertyService {
       additional_bindings {get: "/v1/property/lists/{container.group}"}
     };
   }
+  rpc KeepAlive(KeepAliveRequest) returns (KeepAliveResponse) {
+    option (google.api.http) = {put: "/v1/property/lease/{lease_id}"};
+  }
 }
diff --git a/banyand/liaison/grpc/property.go b/banyand/liaison/grpc/property.go
index 05a542f8..6c0bcbc5 100644
--- a/banyand/liaison/grpc/property.go
+++ b/banyand/liaison/grpc/property.go
@@ -30,11 +30,11 @@ type propertyServer struct {
 }
 
 func (ps *propertyServer) Apply(ctx context.Context, req 
*propertyv1.ApplyRequest) (*propertyv1.ApplyResponse, error) {
-       created, tagsNum, err := 
ps.schemaRegistry.PropertyRegistry().ApplyProperty(ctx, req.Property, 
req.Strategy)
+       created, tagsNum, leaseID, err := 
ps.schemaRegistry.PropertyRegistry().ApplyProperty(ctx, req.Property, 
req.Strategy)
        if err != nil {
                return nil, err
        }
-       return &propertyv1.ApplyResponse{Created: created, TagsNum: tagsNum}, 
nil
+       return &propertyv1.ApplyResponse{Created: created, TagsNum: tagsNum, 
LeaseId: leaseID}, nil
 }
 
 func (ps *propertyServer) Delete(ctx context.Context, req 
*propertyv1.DeleteRequest) (*propertyv1.DeleteResponse, error) {
@@ -67,3 +67,11 @@ func (ps *propertyServer) List(ctx context.Context, req 
*propertyv1.ListRequest)
                Property: entities,
        }, nil
 }
+
+func (ps *propertyServer) KeepAlive(ctx context.Context, req 
*propertyv1.KeepAliveRequest) (*propertyv1.KeepAliveResponse, error) {
+       err := ps.schemaRegistry.PropertyRegistry().KeepAlive(ctx, 
req.GetLeaseId())
+       if err != nil {
+               return nil, err
+       }
+       return &propertyv1.KeepAliveResponse{}, nil
+}
diff --git a/banyand/metadata/schema/property.go 
b/banyand/metadata/schema/property.go
index 44dad59b..906d3e59 100644
--- a/banyand/metadata/schema/property.go
+++ b/banyand/metadata/schema/property.go
@@ -105,15 +105,15 @@ func (e *etcdSchemaRegistry) ListProperty(ctx 
context.Context, container *common
        return entities, nil
 }
 
-func (e *etcdSchemaRegistry) ApplyProperty(ctx context.Context, property 
*propertyv1.Property, strategy propertyv1.ApplyRequest_Strategy) (bool, uint32, 
error) {
+func (e *etcdSchemaRegistry) ApplyProperty(ctx context.Context, property 
*propertyv1.Property, strategy propertyv1.ApplyRequest_Strategy) (bool, uint32, 
int64, error) {
        if !e.closer.AddRunning() {
-               return false, 0, ErrClosed
+               return false, 0, 0, ErrClosed
        }
        defer e.closer.Done()
        m := transformKey(property.GetMetadata())
        group := m.GetGroup()
        if _, getGroupErr := e.GetGroup(ctx, group); getGroupErr != nil {
-               return false, 0, errors.Wrap(getGroupErr, "group is not exist")
+               return false, 0, 0, errors.Wrap(getGroupErr, "group is not 
exist")
        }
        md := Metadata{
                TypeMeta: TypeMeta{
@@ -125,17 +125,17 @@ func (e *etcdSchemaRegistry) ApplyProperty(ctx 
context.Context, property *proper
        }
        key, err := md.key()
        if err != nil {
-               return false, 0, err
+               return false, 0, 0, err
        }
        key = e.prependNamespace(key)
        var ttl int64
        if property.Ttl != "" {
                t, err := timestamp.ParseDuration(property.Ttl)
                if err != nil {
-                       return false, 0, err
+                       return false, 0, 0, err
                }
                if t < time.Second {
-                       return false, 0, errors.New("ttl should be greater than 
1s")
+                       return false, 0, 0, errors.New("ttl should be greater 
than 1s")
                }
                ttl = int64(t / time.Second)
        }
@@ -145,28 +145,37 @@ func (e *etcdSchemaRegistry) ApplyProperty(ctx 
context.Context, property *proper
        return e.mergeProperty(ctx, key, property, ttl)
 }
 
-func (e *etcdSchemaRegistry) replaceProperty(ctx context.Context, key string, 
property *propertyv1.Property, ttl int64) (bool, uint32, error) {
-       val, opts, err := e.marshalProperty(ctx, property, ttl)
+func (e *etcdSchemaRegistry) replaceProperty(ctx context.Context, key string, 
property *propertyv1.Property, ttl int64) (bool, uint32, int64, error) {
+       leaseID, err := e.grant(ctx, ttl)
        if err != nil {
-               return false, 0, err
+               return false, 0, 0, err
        }
-       _, err = e.client.Put(ctx, key, string(val), opts...)
+       property.LeaseId = leaseID
+       val, err := e.marshalProperty(property)
        if err != nil {
-               return false, 0, err
+               return false, 0, 0, err
        }
-       return true, uint32(len(property.Tags)), nil
+       _, err = e.client.Put(ctx, key, string(val), 
clientv3.WithLease(clientv3.LeaseID(leaseID)))
+       if err != nil {
+               return false, 0, 0, err
+       }
+       return true, uint32(len(property.Tags)), leaseID, nil
 }
 
-func (e *etcdSchemaRegistry) mergeProperty(ctx context.Context, key string, 
property *propertyv1.Property, ttl int64) (bool, uint32, error) {
+func (e *etcdSchemaRegistry) mergeProperty(ctx context.Context, key string, 
property *propertyv1.Property, ttl int64) (bool, uint32, int64, error) {
        tagsNum := uint32(len(property.Tags))
-       existed, errGet := e.GetProperty(ctx, property.Metadata, nil)
-       if errors.Is(errGet, ErrGRPCResourceNotFound) {
+       existed, err := e.GetProperty(ctx, property.Metadata, nil)
+       if errors.Is(err, ErrGRPCResourceNotFound) {
                return e.replaceProperty(ctx, key, property, ttl)
        }
-       if errGet != nil {
-               return false, 0, errGet
+       if err != nil {
+               return false, 0, 0, err
+       }
+       prevLeaseID := existed.LeaseId
+       leaseID, err := e.grant(ctx, ttl)
+       if err != nil {
+               return false, 0, 0, err
        }
-       var prevLeaseID int64
        merge := func(existed *propertyv1.Property) (*propertyv1.Property, 
error) {
                tags := make([]*modelv1.Tag, len(property.Tags))
                copy(tags, property.Tags)
@@ -180,20 +189,20 @@ func (e *etcdSchemaRegistry) mergeProperty(ctx 
context.Context, key string, prop
                        }
                }
                existed.Tags = append(existed.Tags, tags...)
-               prevLeaseID = existed.LeaseId
-               val, opts, err := e.marshalProperty(ctx, existed, ttl)
-               if err != nil {
-                       return nil, err
+               existed.LeaseId = leaseID
+               val, errMerge := e.marshalProperty(existed)
+               if errMerge != nil {
+                       return nil, errMerge
                }
-               txnResp, err := e.client.Txn(ctx).If(
+               txnResp, errMerge := e.client.Txn(ctx).If(
                        clientv3.Compare(clientv3.ModRevision(key), "=", 
existed.Metadata.Container.ModRevision),
                ).Then(
-                       clientv3.OpPut(key, string(val), opts...),
+                       clientv3.OpPut(key, string(val), 
clientv3.WithLease(clientv3.LeaseID(leaseID))),
                ).Else(
                        clientv3.OpGet(key),
                ).Commit()
-               if err != nil {
-                       return nil, err
+               if errMerge != nil {
+                       return nil, errMerge
                }
                if txnResp.Succeeded {
                        return nil, nil
@@ -203,20 +212,19 @@ func (e *etcdSchemaRegistry) mergeProperty(ctx 
context.Context, key string, prop
                        return nil, ErrGRPCResourceNotFound
                }
                p := new(propertyv1.Property)
-               if err = proto.Unmarshal(getResp.Kvs[0].Value, p); err != nil {
-                       return nil, err
+               if errMerge = proto.Unmarshal(getResp.Kvs[0].Value, p); 
errMerge != nil {
+                       return nil, errMerge
                }
                return p, nil
        }
        // Self-spin to merge property
-       var err error
        for i := 0; i < 10; i++ {
                existed, err = merge(existed)
                if errors.Is(err, ErrGRPCResourceNotFound) {
                        return e.replaceProperty(ctx, key, property, ttl)
                }
                if err != nil {
-                       return false, 0, err
+                       return false, 0, 0, err
                }
                if existed == nil {
                        break
@@ -224,30 +232,32 @@ func (e *etcdSchemaRegistry) mergeProperty(ctx 
context.Context, key string, prop
                time.Sleep(time.Millisecond * 100)
        }
        if existed != nil {
-               return false, 0, errors.New("merge property failed: retry 
timeout")
+               return false, 0, 0, errors.New("merge property failed: retry 
timeout")
        }
        if prevLeaseID > 0 {
                _, _ = e.client.Revoke(ctx, clientv3.LeaseID(prevLeaseID))
        }
-       return false, tagsNum, nil
+       return false, tagsNum, leaseID, nil
 }
 
-func (e *etcdSchemaRegistry) marshalProperty(ctx context.Context, property 
*propertyv1.Property, ttl int64) ([]byte, []clientv3.OpOption, error) {
-       var opts []clientv3.OpOption
-       if ttl > 0 {
-               lease, err := e.client.Grant(ctx, ttl)
-               if err != nil {
-                       return nil, nil, err
-               }
-               property.LeaseId = int64(lease.ID)
-               opts = append(opts, clientv3.WithLease(lease.ID))
+func (e *etcdSchemaRegistry) grant(ctx context.Context, ttl int64) (int64, 
error) {
+       if ttl < 1 {
+               return 0, nil
+       }
+       lease, err := e.client.Grant(ctx, ttl)
+       if err != nil {
+               return 0, err
        }
+       return int64(lease.ID), nil
+}
+
+func (e *etcdSchemaRegistry) marshalProperty(property *propertyv1.Property) 
([]byte, error) {
        property.UpdatedAt = timestamppb.Now()
        val, err := proto.Marshal(property)
        if err != nil {
-               return nil, nil, err
+               return nil, err
        }
-       return val, opts, nil
+       return val, nil
 }
 
 func (e *etcdSchemaRegistry) DeleteProperty(ctx context.Context, metadata 
*propertyv1.Metadata, tags []string) (bool, uint32, error) {
@@ -278,10 +288,19 @@ func (e *etcdSchemaRegistry) DeleteProperty(ctx 
context.Context, metadata *prope
                        }
                }
        }
-       _, num, err := e.ApplyProperty(ctx, filtered, 
propertyv1.ApplyRequest_STRATEGY_REPLACE)
+       _, num, _, err := e.ApplyProperty(ctx, filtered, 
propertyv1.ApplyRequest_STRATEGY_REPLACE)
        return true, num, err
 }
 
+func (e *etcdSchemaRegistry) KeepAlive(ctx context.Context, leaseID int64) 
error {
+       if !e.closer.AddRunning() {
+               return ErrClosed
+       }
+       defer e.closer.Done()
+       _, err := e.client.KeepAliveOnce(ctx, clientv3.LeaseID(leaseID))
+       return err
+}
+
 func transformKey(metadata *propertyv1.Metadata) *commonv1.Metadata {
        return &commonv1.Metadata{
                Group: metadata.Container.GetGroup(),
diff --git a/banyand/metadata/schema/schema.go 
b/banyand/metadata/schema/schema.go
index 6806b1a5..0c7095af 100644
--- a/banyand/metadata/schema/schema.go
+++ b/banyand/metadata/schema/schema.go
@@ -193,8 +193,9 @@ type TopNAggregation interface {
 type Property interface {
        GetProperty(ctx context.Context, metadata *propertyv1.Metadata, tags 
[]string) (*propertyv1.Property, error)
        ListProperty(ctx context.Context, container *commonv1.Metadata, ids 
[]string, tags []string) ([]*propertyv1.Property, error)
-       ApplyProperty(ctx context.Context, property *propertyv1.Property, 
strategy propertyv1.ApplyRequest_Strategy) (bool, uint32, error)
+       ApplyProperty(ctx context.Context, property *propertyv1.Property, 
strategy propertyv1.ApplyRequest_Strategy) (bool, uint32, int64, error)
        DeleteProperty(ctx context.Context, metadata *propertyv1.Metadata, tags 
[]string) (bool, uint32, error)
+       KeepAlive(ctx context.Context, leaseID int64) error
 }
 
 // Node allows CRUD node schemas in a group.
diff --git a/bydbctl/internal/cmd/property.go b/bydbctl/internal/cmd/property.go
index 27c8420d..529f68f5 100644
--- a/bydbctl/internal/cmd/property.go
+++ b/bydbctl/internal/cmd/property.go
@@ -18,6 +18,8 @@
 package cmd
 
 import (
+       "strconv"
+
        "github.com/go-resty/resty/v2"
        "github.com/spf13/cobra"
        "google.golang.org/protobuf/encoding/protojson"
@@ -116,6 +118,25 @@ func newPropertyCmd() *cobra.Command {
        listCmd.Flags().StringArrayVarP(&ids, "ids", "", nil, "id selector")
        listCmd.Flags().StringArrayVarP(&tags, "tags", "t", nil, "tag selector")
 
-       propertyCmd.AddCommand(getCmd, applyCmd, deleteCmd, listCmd)
+       var leaseID int64
+       keepAliveCmd := &cobra.Command{
+               Use:     "keepalive -i lease_id",
+               Version: version.Build(),
+               Short:   "Keep alive a property",
+               RunE: func(_ *cobra.Command, _ []string) (err error) {
+                       return rest(func() ([]reqBody, error) {
+                               if leaseID == 0 {
+                                       return nil, errMalformedInput
+                               }
+                               return []reqBody{{leaseID: leaseID}}, nil
+                       }, func(request request) (*resty.Response, error) {
+                               return request.req.SetPathParam("lease_id", 
strconv.FormatInt(request.leaseID, 10)).
+                                       Put(getPath(propertySchemaPath + 
"/lease/{lease_id}"))
+                       }, yamlPrinter)
+               },
+       }
+       keepAliveCmd.Flags().Int64VarP(&leaseID, "lease_id", "i", 0, "the lease 
id of the property")
+
+       propertyCmd.AddCommand(getCmd, applyCmd, deleteCmd, listCmd, 
keepAliveCmd)
        return propertyCmd
 }
diff --git a/bydbctl/internal/cmd/property_test.go 
b/bydbctl/internal/cmd/property_test.go
index 86fc241a..e16a2750 100644
--- a/bydbctl/internal/cmd/property_test.go
+++ b/bydbctl/internal/cmd/property_test.go
@@ -18,6 +18,7 @@
 package cmd_test
 
 import (
+       "strconv"
        "strings"
 
        "github.com/google/go-cmp/cmp"
@@ -39,6 +40,7 @@ var equalsOpts = []cmp.Option{
        protocmp.Transform(),
        protocmp.IgnoreUnknown(),
        protocmp.IgnoreFields(&propertyv1.Property{}, "updated_at"),
+       protocmp.IgnoreFields(&propertyv1.Property{}, "lease_id"),
        protocmp.IgnoreFields(&commonv1.Metadata{}, "mod_revision"),
        protocmp.IgnoreFields(&commonv1.Metadata{}, "create_revision"),
 }
@@ -79,6 +81,20 @@ tags:
       int:
         value: 3
 `
+       p3Yaml := `
+metadata:
+  container:
+    group: ui-template
+    name: security
+  id: login-token
+tags:
+  - key: content
+    value:
+      str:
+        value: foo 
+ttl: 30m
+`
+
        p1Proto := new(propertyv1.Property)
        helpers.UnmarshalYAML([]byte(p1YAML), p1Proto)
        p2Proto := new(propertyv1.Property)
@@ -280,7 +296,36 @@ tags:
                helpers.UnmarshalYAML([]byte(out), resp)
                Expect(resp.Property).To(HaveLen(2))
        })
-
+       It("keepalive not found", func() {
+               rootCmd.SetArgs([]string{
+                       "property", "keepalive", "-i", "111",
+               })
+               out := capturer.CaptureStdout(func() {
+                       err := rootCmd.Execute()
+                       Expect(err).Should(MatchError("rpc error: code = 
Unknown desc = etcdserver: requested lease not found"))
+               })
+               GinkgoWriter.Println(out)
+       })
+       It("keepalive", func() {
+               rootCmd.SetArgs([]string{"property", "apply", "-a", addr, "-f", 
"-"})
+               rootCmd.SetIn(strings.NewReader(p3Yaml))
+               out := capturer.CaptureStdout(func() {
+                       err := rootCmd.Execute()
+                       Expect(err).NotTo(HaveOccurred())
+               })
+               GinkgoWriter.Println(out)
+               resp := new(propertyv1.ApplyResponse)
+               helpers.UnmarshalYAML([]byte(out), resp)
+               Expect(resp.LeaseId).Should(BeNumerically(">", 0))
+               rootCmd.SetArgs([]string{
+                       "property", "keepalive", "-i", 
strconv.Itoa(int(resp.LeaseId)),
+               })
+               out = capturer.CaptureStdout(func() {
+                       err := rootCmd.Execute()
+                       Expect(err).NotTo(HaveOccurred())
+               })
+               GinkgoWriter.Println(out)
+       })
        AfterEach(func() {
                deferFunc()
        })
diff --git a/bydbctl/internal/cmd/rest.go b/bydbctl/internal/cmd/rest.go
index e161bdda..02e15e86 100644
--- a/bydbctl/internal/cmd/rest.go
+++ b/bydbctl/internal/cmd/rest.go
@@ -61,6 +61,7 @@ type reqBody struct {
        tags       []string
        parsedData map[string]interface{}
        data       []byte
+       leaseID    int64
 }
 
 type request struct {
diff --git a/docs/api-reference.md b/docs/api-reference.md
index c7dae331..24550676 100644
--- a/docs/api-reference.md
+++ b/docs/api-reference.md
@@ -191,6 +191,8 @@
     - [DeleteResponse](#banyandb-property-v1-DeleteResponse)
     - [GetRequest](#banyandb-property-v1-GetRequest)
     - [GetResponse](#banyandb-property-v1-GetResponse)
+    - [KeepAliveRequest](#banyandb-property-v1-KeepAliveRequest)
+    - [KeepAliveResponse](#banyandb-property-v1-KeepAliveResponse)
     - [ListRequest](#banyandb-property-v1-ListRequest)
     - [ListResponse](#banyandb-property-v1-ListResponse)
   
@@ -2750,6 +2752,7 @@ Property stores the user defined data
 | ----- | ---- | ----- | ----------- |
 | created | [bool](#bool) |  | created indicates whether the property existed. 
True: the property is absent. False: the property existed. |
 | tags_num | [uint32](#uint32) |  |  |
+| lease_id | [int64](#int64) |  |  |
 
 
 
@@ -2819,6 +2822,31 @@ Property stores the user defined data
 
 
 
+<a name="banyandb-property-v1-KeepAliveRequest"></a>
+
+### KeepAliveRequest
+
+
+
+| Field | Type | Label | Description |
+| ----- | ---- | ----- | ----------- |
+| lease_id | [int64](#int64) |  |  |
+
+
+
+
+
+
+<a name="banyandb-property-v1-KeepAliveResponse"></a>
+
+### KeepAliveResponse
+
+
+
+
+
+
+
 <a name="banyandb-property-v1-ListRequest"></a>
 
 ### ListRequest
@@ -2881,6 +2909,7 @@ Property stores the user defined data
 | Delete | [DeleteRequest](#banyandb-property-v1-DeleteRequest) | 
[DeleteResponse](#banyandb-property-v1-DeleteResponse) |  |
 | Get | [GetRequest](#banyandb-property-v1-GetRequest) | 
[GetResponse](#banyandb-property-v1-GetResponse) |  |
 | List | [ListRequest](#banyandb-property-v1-ListRequest) | 
[ListResponse](#banyandb-property-v1-ListResponse) |  |
+| KeepAlive | [KeepAliveRequest](#banyandb-property-v1-KeepAliveRequest) | 
[KeepAliveResponse](#banyandb-property-v1-KeepAliveResponse) |  |
 
  
 
diff --git a/docs/concept/data-model.md b/docs/concept/data-model.md
index 06b719fb..4c63f2ed 100644
--- a/docs/concept/data-model.md
+++ b/docs/concept/data-model.md
@@ -198,6 +198,34 @@ tags:
 
 `Property` supports a three-level hierarchy, `group`/`name`/`id`, that is more 
flexible than schemaful data models.
 
+The property supports the TTL mechanism. You could set the `ttl` field to 
specify the time to live.
+
+```yaml
+metadata:
+  container:
+    group: sw
+    name: ui_template
+  id: General-Service
+tags:
+- key: name
+  value:
+    str:
+      value: "hello"
+- key: state
+  value:
+    str:
+      value: "succeed"
+ttl: "1h"
+```
+
+"General-Service" will be dropped after 1 hour. If you want to extend the TTL, 
you could use the "keepalive" operation. The "lease_id" is returned in the 
apply response. You can use get operation to get the property with the lease_id 
as well.
+
+```yaml
+lease_id: 1
+```
+
+"General-Service" lives another 1 hour.
+
 You could Create, Read, Update and Drop a property, and update or drop several 
tags instead of the entire property.
 
 [Property Operations](../api-reference.md#propertyservice)
diff --git a/docs/crud/property.md b/docs/crud/property.md
index 4c010e7c..6bf627f4 100644
--- a/docs/crud/property.md
+++ b/docs/crud/property.md
@@ -128,6 +128,45 @@ List operation lists all properties in a group with a name.
 $ bydbctl property list -g sw -n ui_template
 ```
 
+## TTL field in a property
+
+TTL field in a property is used to set the time to live of the property. The 
property will be deleted automatically after the TTL.
+
+This functionality is supported by the lease mechanism. The readonly lease_id 
field is used to identify the lease of the property.
+
+### Examples of setting TTL
+
+```shell
+$ bydbctl property apply -f - <<EOF
+metadata:
+  container:
+    group: sw
+    name: ui_template
+  id: General-Service
+tags:
+- key: state
+  value:
+    str:
+      value: "failed"
+ttl: "1h"
+EOF
+```
+
+The lease_id is returned in the response. 
+You can use get operation to get the property with the lease_id as well.
+
+```shell
+$ bydbctl property get -g sw -n ui_template --id General-Service
+```
+
+The lease_id is used to keep the property alive. You can use keepalive 
operation to keep the property alive.
+When the keepalive operation is called, the property's TTL will be reset to 
the original value.
+
+```shell
+$ bydbctl property keepalive --lease_id 1
+```
+
+
 ## API Reference
 
 [MeasureService v1](../../api-reference.md#PropertyService)
diff --git a/test/integration/standalone/other/property_test.go 
b/test/integration/standalone/other/property_test.go
index 2e614911..9b5ad68c 100644
--- a/test/integration/standalone/other/property_test.go
+++ b/test/integration/standalone/other/property_test.go
@@ -120,6 +120,7 @@ var _ = Describe("Property application", func() {
                Expect(err).NotTo(HaveOccurred())
                Expect(resp.Created).To(BeTrue())
                Expect(resp.TagsNum).To(Equal(uint32(2)))
+               Expect(resp.LeaseId).To(BeNumerically(">", 0))
                got, err := client.Get(context.Background(), 
&propertyv1.GetRequest{Metadata: md})
                Expect(err).NotTo(HaveOccurred())
                Expect(got.Property.Tags).To(Equal([]*modelv1.Tag{
@@ -127,7 +128,7 @@ var _ = Describe("Property application", func() {
                        {Key: "t2", Value: &modelv1.TagValue{Value: 
&modelv1.TagValue_Str{Str: &modelv1.Str{Value: "v2"}}}},
                }))
                Expect(got.Property.GetTtl()).To(Equal("1h"))
-               Expect(got.Property.GetLeaseId()).To(BeNumerically(">", 0))
+               Expect(got.Property.GetLeaseId()).To(Equal(resp.LeaseId))
                resp, err = client.Apply(context.Background(), 
&propertyv1.ApplyRequest{Property: &propertyv1.Property{
                        Metadata: md,
                        Tags: []*modelv1.Tag{
@@ -143,6 +144,31 @@ var _ = Describe("Property application", func() {
                        return err
                }, flags.EventuallyTimeout).Should(MatchError("rpc error: code 
= NotFound desc = banyandb: resource not found"))
        })
+       It("keeps alive", func() {
+               _, err := client.KeepAlive(context.Background(), 
&propertyv1.KeepAliveRequest{LeaseId: 0})
+               Expect(err).Should(MatchError("rpc error: code = Unknown desc = 
etcdserver: requested lease not found"))
+               md := &propertyv1.Metadata{
+                       Container: &commonv1.Metadata{
+                               Name:  "p",
+                               Group: "g",
+                       },
+                       Id: "1",
+               }
+               resp, err := client.Apply(context.Background(), 
&propertyv1.ApplyRequest{Property: &propertyv1.Property{
+                       Metadata: md,
+                       Tags: []*modelv1.Tag{
+                               {Key: "t1", Value: &modelv1.TagValue{Value: 
&modelv1.TagValue_Str{Str: &modelv1.Str{Value: "v1"}}}},
+                               {Key: "t2", Value: &modelv1.TagValue{Value: 
&modelv1.TagValue_Str{Str: &modelv1.Str{Value: "v2"}}}},
+                       },
+                       Ttl: "30m",
+               }})
+               Expect(err).NotTo(HaveOccurred())
+               Expect(resp.Created).To(BeTrue())
+               Expect(resp.TagsNum).To(Equal(uint32(2)))
+               Expect(resp.LeaseId).To(BeNumerically(">", 0))
+               _, err = client.KeepAlive(context.Background(), 
&propertyv1.KeepAliveRequest{LeaseId: resp.LeaseId})
+               Expect(err).NotTo(HaveOccurred())
+       })
 })
 
 var _ = Describe("Property application", func() {

Reply via email to