diff --git a/pkg/document/change/context.go b/pkg/document/change/context.go index c494644fb..79d9585fb 100644 --- a/pkg/document/change/context.go +++ b/pkg/document/change/context.go @@ -85,6 +85,11 @@ func (c *Context) RegisterElementHasRemovedNodes(element crdt.GCElement) { c.root.RegisterElementHasRemovedNodes(element) } +// RegisterGCNodePairMapByID register the given GCNode pair to hash table. +func (c *Context) RegisterGCNodePairMapByID(key string, parent crdt.GCNode, child crdt.GCNode) { + c.root.RegisterGCNodePairMapByID(key, parent, child) +} + // LastTimeTicket returns the last time ticket issued by this context. func (c *Context) LastTimeTicket() *time.Ticket { return c.id.NewTimeTicket(c.delimiter) diff --git a/pkg/document/crdt/element_rht.go b/pkg/document/crdt/element_rht.go index 8186bc3f0..ff5b0feb6 100644 --- a/pkg/document/crdt/element_rht.go +++ b/pkg/document/crdt/element_rht.go @@ -110,7 +110,7 @@ func (rht *ElementRHT) Set(k string, v Element) Element { if !ok || v.CreatedAt().After(node.elem.CreatedAt()) { rht.nodeMapByKey[k] = newNode } - + // TODO(raararaara): Overwritten node does not need to be purged. return removed } diff --git a/pkg/document/crdt/gc_node.go b/pkg/document/crdt/gc_node.go new file mode 100644 index 000000000..2b50956bb --- /dev/null +++ b/pkg/document/crdt/gc_node.go @@ -0,0 +1,15 @@ +package crdt + +import "github.com/yorkie-team/yorkie/pkg/document/time" + +// GCNode represents a common node with GC. +type GCNode interface { + // GetID returns the IDString of this node. + GetID() string + + // GetRemovedAt returns the removal time of this node. + GetRemovedAt() *time.Ticket + + // Purge physically purges the given child of this node. + Purge(node GCNode, ticket *time.Ticket) (int, error) +} diff --git a/pkg/document/crdt/rht.go b/pkg/document/crdt/rht.go index bc424e18e..a10199ab8 100644 --- a/pkg/document/crdt/rht.go +++ b/pkg/document/crdt/rht.go @@ -41,6 +41,18 @@ func newRHTNode(key, val string, updatedAt *time.Ticket, isRemoved bool) *RHTNod } } +// Remove removes this node. It marks RHTNode as removed and updates the value of `updatedAt` (tombstone). +func (n *RHTNode) Remove(removedAt *time.Ticket) bool { + if removedAt != nil && removedAt.After(n.UpdatedAt()) && + !n.isRemoved || removedAt.After(n.UpdatedAt()) { + n.isRemoved = true + n.updatedAt = removedAt + + return true + } + return false +} + // Key returns the key of this node. func (n *RHTNode) Key() string { return n.key @@ -56,19 +68,38 @@ func (n *RHTNode) UpdatedAt() *time.Ticket { return n.updatedAt } +// GetID returns the IDString of this node. +func (n *RHTNode) GetID() string { + return n.updatedAt.Key() + ":" + n.key +} + +// GetRemovedAt returns the removal time of this node. +func (n *RHTNode) GetRemovedAt() *time.Ticket { + if n.isRemoved { + return n.updatedAt + } + return nil +} + +// Purge physically purges children of given node. +// NOTE(raararaara): This method was added only for consistency. +// It will not be used unless additional children of RHTNode are implemented. +func (n *RHTNode) Purge(_ GCNode, _ *time.Ticket) (int, error) { + return 0, nil +} + // RHT is a hashtable with logical clock(Replicated hashtable). // For more details about RHT: http://csl.skku.edu/papers/jpdc11.pdf // NOTE(justiceHui): RHT and ElementRHT has duplicated functions. type RHT struct { - nodeMapByKey map[string]*RHTNode - numberOfRemovedElement int + // nodeMapByKey is a map with values of nodes by key. + nodeMapByKey map[string]*RHTNode } // NewRHT creates a new instance of RHT. func NewRHT() *RHT { return &RHT{ - nodeMapByKey: make(map[string]*RHTNode), - numberOfRemovedElement: 0, + nodeMapByKey: make(map[string]*RHTNode), } } @@ -96,39 +127,27 @@ func (rht *RHT) Has(key string) bool { // Set sets the value of the given key. func (rht *RHT) Set(k, v string, executedAt *time.Ticket) { if node, ok := rht.nodeMapByKey[k]; !ok || executedAt.After(node.updatedAt) { - if node != nil && node.isRemoved { - rht.numberOfRemovedElement-- - } newNode := newRHTNode(k, v, executedAt, false) rht.nodeMapByKey[k] = newNode } } // Remove removes the Element of the given key. -func (rht *RHT) Remove(k string, executedAt *time.Ticket) string { - if node, ok := rht.nodeMapByKey[k]; !ok || executedAt.After(node.updatedAt) { - // NOTE(justiceHui): Even if key is not existed, we must set flag `isRemoved` for concurrency - if node == nil { - rht.numberOfRemovedElement++ +func (rht *RHT) Remove(k string, executedAt *time.Ticket) *RHTNode { + if node, ok := rht.nodeMapByKey[k]; !ok || node.Remove(executedAt) { + if !ok { newNode := newRHTNode(k, ``, executedAt, true) rht.nodeMapByKey[k] = newNode - return "" - } - alreadyRemoved := node.isRemoved - if !alreadyRemoved { - rht.numberOfRemovedElement++ + return newNode } newNode := newRHTNode(k, node.val, executedAt, true) rht.nodeMapByKey[k] = newNode - if alreadyRemoved { - return "" - } - return node.val + return newNode } - return "" + return nil } // Elements returns a map of elements because the map easy to use for loop. @@ -144,22 +163,35 @@ func (rht *RHT) Elements() map[string]string { return members } -// Nodes returns a map of elements because the map easy to use for loop. +// Nodes returns a list of RHTNodes. // TODO: If we encounter performance issues, we need to replace this with other solution. func (rht *RHT) Nodes() []*RHTNode { var nodes []*RHTNode for _, node := range rht.nodeMapByKey { - if !node.isRemoved { - nodes = append(nodes, node) - } + nodes = append(nodes, node) } return nodes } +func (rht *RHT) purge(key string, ticket *time.Ticket) (int, error) { + if node, ok := rht.nodeMapByKey[key]; ok && ticket.After(node.updatedAt) { + delete(rht.nodeMapByKey, key) + return 1, nil + } + + return 0, nil +} + // Len returns the number of elements. func (rht *RHT) Len() int { - return len(rht.nodeMapByKey) - rht.numberOfRemovedElement + count := 0 + for _, node := range rht.Nodes() { + if !node.isRemoved { + count++ + } + } + return count } // DeepCopy copies itself deeply. diff --git a/pkg/document/crdt/rht_test.go b/pkg/document/crdt/rht_test.go index f7c90f88c..f4cbcb898 100644 --- a/pkg/document/crdt/rht_test.go +++ b/pkg/document/crdt/rht_test.go @@ -141,84 +141,103 @@ func TestRHT_Remove(t *testing.T) { key2, val2, val22 := `key2`, `value2`, `value22` tests := []struct { - desc string - insertKey []string - insertVal []string - deleteKey []string - deleteVal []string - expectXML string - expectJSON string - expectSize int + desc string + insertKey []string + insertVal []string + deleteKey []string + deleteVal []string + expectXML string + expectJSON string + expectSize int + expectTotalSize int }{ { - desc: `1. set elements`, - insertKey: []string{key1, key2}, - insertVal: []string{val1, val2}, - deleteKey: []string{}, - deleteVal: []string{}, - expectXML: `key1="value1" key2="value2"`, - expectJSON: `{"key1":"value1","key2":"value2"}`, - expectSize: 2, + desc: `1. set single element`, + insertKey: []string{key1}, + insertVal: []string{val1}, + deleteKey: []string{}, + deleteVal: []string{}, + expectXML: `key1="value1"`, + expectJSON: `{"key1":"value1"}`, + expectSize: 1, + expectTotalSize: 1, }, { - desc: `2. remove element`, - insertKey: []string{}, - insertVal: []string{}, - deleteKey: []string{key1}, - deleteVal: []string{val1}, - expectXML: `key2="value2"`, - expectJSON: `{"key2":"value2"}`, - expectSize: 1, + desc: `2. set another element`, + insertKey: []string{key2}, + insertVal: []string{val2}, + deleteKey: []string{}, + deleteVal: []string{}, + expectXML: `key1="value1" key2="value2"`, + expectJSON: `{"key1":"value1","key2":"value2"}`, + expectSize: 2, + expectTotalSize: 2, }, { - desc: `3. set after remove`, - insertKey: []string{key1}, - insertVal: []string{val11}, - deleteKey: []string{}, - deleteVal: []string{}, - expectXML: `key1="value11" key2="value2"`, - expectJSON: `{"key1":"value11","key2":"value2"}`, - expectSize: 2, + desc: `3. remove element`, + insertKey: []string{}, + insertVal: []string{}, + deleteKey: []string{key1}, + deleteVal: []string{val1}, + expectXML: `key2="value2"`, + expectJSON: `{"key2":"value2"}`, + expectSize: 1, + expectTotalSize: 2, + }, + { + desc: `4. set after remove`, + insertKey: []string{key1}, + insertVal: []string{val11}, + deleteKey: []string{}, + deleteVal: []string{}, + expectXML: `key1="value11" key2="value2"`, + expectJSON: `{"key1":"value11","key2":"value2"}`, + expectSize: 2, + expectTotalSize: 2, + }, + { + desc: `5. remove element`, + insertKey: []string{key2}, + insertVal: []string{val22}, + deleteKey: []string{key1}, + deleteVal: []string{val11}, + expectXML: `key2="value22"`, + expectJSON: `{"key2":"value22"}`, + expectSize: 1, + expectTotalSize: 2, + }, + { + desc: `6. remove element again`, + insertKey: []string{}, + insertVal: []string{}, + deleteKey: []string{key1}, + deleteVal: []string{val11}, + expectXML: `key2="value22"`, + expectJSON: `{"key2":"value22"}`, + expectSize: 1, + expectTotalSize: 2, + }, + { + desc: `7. remove element(cleared)`, + insertKey: []string{}, + insertVal: []string{}, + deleteKey: []string{key2}, + deleteVal: []string{val22}, + expectXML: ``, + expectJSON: `{}`, + expectSize: 0, + expectTotalSize: 2, }, { - desc: `4. remove element`, - insertKey: []string{key2}, - insertVal: []string{val22}, - deleteKey: []string{key1}, - deleteVal: []string{val11}, - expectXML: `key2="value22"`, - expectJSON: `{"key2":"value22"}`, - expectSize: 1, - }, - { - desc: `5. remove element again`, - insertKey: []string{}, - insertVal: []string{}, - deleteKey: []string{key1}, - deleteVal: []string{``}, - expectXML: `key2="value22"`, - expectJSON: `{"key2":"value22"}`, - expectSize: 1, - }, - { - desc: `6. remove element(cleared)`, - insertKey: []string{}, - insertVal: []string{}, - deleteKey: []string{key2}, - deleteVal: []string{val22}, - expectXML: ``, - expectJSON: `{}`, - expectSize: 0, - }, - { - desc: `7. remove not exist key`, - insertKey: []string{}, - insertVal: []string{}, - deleteKey: []string{`not-exist-key`}, - deleteVal: []string{``}, - expectXML: ``, - expectJSON: `{}`, - expectSize: 0, + desc: `8. remove not exist key`, + insertKey: []string{}, + insertVal: []string{}, + deleteKey: []string{`not-exist-key`}, + deleteVal: []string{``}, + expectXML: ``, + expectJSON: `{}`, + expectSize: 0, + expectTotalSize: 3, }, } @@ -234,13 +253,15 @@ func TestRHT_Remove(t *testing.T) { } for i, key := range tt.deleteKey { removedElement := rht.Remove(key, ctx.IssueTimeTicket()) - assert.Equal(t, tt.deleteVal[i], removedElement) + if removedElement != nil { + assert.Equal(t, tt.deleteVal[i], removedElement.Value()) + } } assert.Equal(t, tt.expectXML, rht.ToXML()) assert.Equal(t, tt.expectJSON, rht.Marshal()) assert.Equal(t, tt.expectSize, rht.Len()) - assert.Equal(t, tt.expectSize, len(rht.Nodes())) assert.Equal(t, tt.expectSize, len(rht.Elements())) + assert.Equal(t, tt.expectTotalSize, len(rht.Nodes())) }) } } diff --git a/pkg/document/crdt/root.go b/pkg/document/crdt/root.go index 73db7de88..59fc35dfd 100644 --- a/pkg/document/crdt/root.go +++ b/pkg/document/crdt/root.go @@ -29,6 +29,13 @@ type ElementPair struct { elem Element } +// GCPair represents pair that has a parent GCNode and child GCNode. +// Actual GC target is child GCNode. +type GCPair struct { + Parent GCNode + Child GCNode +} + // Root is a structure represents the root of JSON. It has a hash table of // all JSON elements to find a specific element when applying remote changes // received from server. @@ -40,6 +47,7 @@ type Root struct { elementMapByCreatedAt map[string]Element removedElementPairMapByCreatedAt map[string]ElementPair elementHasRemovedNodesSetByCreatedAt map[string]GCElement + gcNodePairMapByID map[string]GCPair } // NewRoot creates a new instance of Root. @@ -48,6 +56,7 @@ func NewRoot(root *Object) *Root { elementMapByCreatedAt: make(map[string]Element), removedElementPairMapByCreatedAt: make(map[string]ElementPair), elementHasRemovedNodesSetByCreatedAt: make(map[string]GCElement), + gcNodePairMapByID: make(map[string]GCPair), } r.object = root @@ -128,6 +137,14 @@ func (r *Root) RegisterElementHasRemovedNodes(element GCElement) { r.elementHasRemovedNodesSetByCreatedAt[element.CreatedAt().Key()] = element } +// RegisterGCNodePairMapByID register the given GCNode pair to hash table. +func (r *Root) RegisterGCNodePairMapByID(key string, parent GCNode, child GCNode) { + r.gcNodePairMapByID[key] = GCPair{ + parent, + child, + } +} + // DeepCopy copies itself deeply. func (r *Root) DeepCopy() (*Root, error) { copiedObject, err := r.object.DeepCopy() @@ -141,6 +158,16 @@ func (r *Root) DeepCopy() (*Root, error) { func (r *Root) GarbageCollect(ticket *time.Ticket) (int, error) { count := 0 + for key, pair := range r.gcNodePairMapByID { + if pair.Child.GetRemovedAt() != nil && ticket.After(pair.Child.GetRemovedAt()) { + purgedNodes, err := pair.Parent.Purge(pair.Child, ticket) + if err != nil { + return 0, err + } + count += purgedNodes + delete(r.gcNodePairMapByID, key) + } + } for _, pair := range r.removedElementPairMapByCreatedAt { if pair.elem.RemovedAt() != nil && ticket.Compare(pair.elem.RemovedAt()) >= 0 { if err := pair.parent.Purge(pair.elem); err != nil { @@ -195,6 +222,8 @@ func (r *Root) GarbageLen() int { count += len(seen) + count += len(r.gcNodePairMapByID) + for _, element := range r.elementHasRemovedNodesSetByCreatedAt { count += element.removedNodesLen() } diff --git a/pkg/document/crdt/tree.go b/pkg/document/crdt/tree.go index ad160a420..370402d3a 100644 --- a/pkg/document/crdt/tree.go +++ b/pkg/document/crdt/tree.go @@ -218,6 +218,16 @@ func (n *TreeNode) Child(offset int) (*TreeNode, error) { return child.Value, nil } +// GetID returns the IDString of this node. +func (n *TreeNode) GetID() string { + return n.ID.toIDString() +} + +// GetRemovedAt returns the removal time of this TreeNode. +func (n *TreeNode) GetRemovedAt() *time.Ticket { + return n.RemovedAt +} + // Split splits the node at the given offset. func (n *TreeNode) Split(tree *Tree, offset int, issueTimeTicket func() *time.Ticket) error { var split *TreeNode @@ -386,6 +396,25 @@ func (n *TreeNode) InsertAfter(content *TreeNode, children *TreeNode) error { return n.Index.InsertAfter(content.Index, children.Index) } +// Purge physically purges RHTNode that have been removed. +func (n *TreeNode) Purge(node GCNode, executedAt *time.Ticket) (int, error) { + if n.Attrs == nil { + return 0, nil + } + + if rhtNode, ok := node.(*RHTNode); ok { + key := rhtNode.key + + count, err := n.Attrs.purge(key, executedAt) + if err != nil { + return 0, err + } + return count, err + } + + return 0, nil +} + // Tree represents the tree of CRDT. It has doubly linked list structure and // index tree structure. type Tree struct { @@ -877,15 +906,21 @@ func (t *Tree) Style(from, to *TreePos, attributes map[string]string, editedAt * } // RemoveStyle removes the given attributes of the given range. -func (t *Tree) RemoveStyle(from, to *TreePos, attributesToRemove []string, editedAt *time.Ticket) error { +func (t *Tree) RemoveStyle( + from, to *TreePos, + attributesToRemove []string, + editedAt *time.Ticket, +) (map[string]GCPair, error) { + gcPair := make(map[string]GCPair) + //nodeHasRemovedRHTNodes := make([]*TreeNode, 0) // 01. split text nodes at the given range if needed. fromParent, fromLeft, err := t.FindTreeNodesWithSplitText(from, editedAt) if err != nil { - return err + return nil, err } toParent, toLeft, err := t.FindTreeNodesWithSplitText(to, editedAt) if err != nil { - return err + return nil, err } err = t.traverseInPosRange(fromParent, fromLeft, toParent, toLeft, @@ -897,15 +932,22 @@ func (t *Tree) RemoveStyle(from, to *TreePos, attributesToRemove []string, edite node.Attrs = NewRHT() } for _, value := range attributesToRemove { - node.Attrs.Remove(value, editedAt) + if removedRhtNode := node.Attrs.Remove(value, editedAt); removedRhtNode != nil { + // NOTE(raararaara): RHTNode does not have a unique key. + // Therefore, it can be made unique by making it dependent on the ID of the TreeNode. + gcPair[node.GetID()+":"+removedRhtNode.GetID()] = GCPair{ + node, + removedRhtNode, + } + } } } }) if err != nil { - return err + return nil, err } - return nil + return gcPair, nil } // FindTreeNodesWithSplitText finds TreeNode of the given crdt.TreePos and diff --git a/pkg/document/json/tree.go b/pkg/document/json/tree.go index 2b118acd4..431a181c0 100644 --- a/pkg/document/json/tree.go +++ b/pkg/document/json/tree.go @@ -256,7 +256,8 @@ func (t *Tree) RemoveStyle(fromIdx, toIdx int, attributesToRemove []string) bool } ticket := t.context.IssueTimeTicket() - if err := t.Tree.RemoveStyle(fromPos, toPos, attributesToRemove, ticket); err != nil { + gcPair, err := t.Tree.RemoveStyle(fromPos, toPos, attributesToRemove, ticket) + if err != nil { panic(err) } @@ -268,6 +269,10 @@ func (t *Tree) RemoveStyle(fromIdx, toIdx int, attributesToRemove []string) bool ticket, )) + for key, pair := range gcPair { + t.context.RegisterGCNodePairMapByID(key, pair.Parent, pair.Child) + } + return true } diff --git a/pkg/document/operations/tree_style.go b/pkg/document/operations/tree_style.go index dce711c04..623177498 100644 --- a/pkg/document/operations/tree_style.go +++ b/pkg/document/operations/tree_style.go @@ -89,8 +89,16 @@ func (e *TreeStyle) Execute(root *crdt.Root) error { if len(e.attributes) > 0 { return obj.Style(e.from, e.to, e.attributes, e.executedAt) } + gcPair, err := obj.RemoveStyle(e.from, e.to, e.attributesToRemove, e.executedAt) + if err != nil { + return err + } + + for k, pair := range gcPair { + root.RegisterGCNodePairMapByID(k, pair.Parent, pair.Child) + } - return obj.RemoveStyle(e.from, e.to, e.attributesToRemove, e.executedAt) + return nil } // FromPos returns the start point of the editing range. diff --git a/test/integration/gc_test.go b/test/integration/gc_test.go index 0c972d5bf..364d93c13 100644 --- a/test/integration/gc_test.go +++ b/test/integration/gc_test.go @@ -383,6 +383,111 @@ func TestGarbageCollection(t *testing.T) { assert.Equal(t, 6, d2.GarbageLen()) }) + t.Run("garbage collection for tree remove style", func(t *testing.T) { + doc := document.New(helper.TestDocKey(t)) + + err := doc.Update(func(root *json.Object, p *presence.Presence) error { + root.SetNewTree("t", &json.TreeNode{ + Type: "root", + Children: []json.TreeNode{ + {Type: "p", Children: []json.TreeNode{{Type: "text", Value: "ab"}}}, + {Type: "p", Attributes: map[string]string{"italic": "true", "bold": "true"}, Children: []json.TreeNode{{Type: "text", Value: "cd"}}}, + }, + }) + assert.Equal(t, `

ab

cd

`, root.GetTree("t").ToXML()) + + return nil + }) + assert.NoError(t, err) + + err = doc.Update(func(root *json.Object, p *presence.Presence) error { + root.GetTree("t").RemoveStyle(4, 8, []string{"italic"}) + assert.Equal(t, `

ab

cd

`, root.GetTree("t").ToXML()) + return nil + }) + assert.NoError(t, err) + assert.Equal(t, 1, doc.GarbageLen()) + assert.Equal(t, 1, doc.GarbageCollect(time.MaxTicket)) + assert.Equal(t, 0, doc.GarbageLen()) + }) + + t.Run("garbage collection for tree remove style (multi node)", func(t *testing.T) { + doc := document.New(helper.TestDocKey(t)) + + err := doc.Update(func(root *json.Object, p *presence.Presence) error { + root.SetNewTree("t", &json.TreeNode{ + Type: "root", + Children: []json.TreeNode{ + {Type: "p", Attributes: map[string]string{"italic": "true"}, Children: []json.TreeNode{{Type: "text", Value: "ab"}}}, + {Type: "p", Attributes: map[string]string{"italic": "true", "bold": "true"}, Children: []json.TreeNode{{Type: "text", Value: "cd"}}}, + }, + }) + assert.Equal(t, `

ab

cd

`, root.GetTree("t").ToXML()) + + return nil + }) + assert.NoError(t, err) + + err = doc.Update(func(root *json.Object, p *presence.Presence) error { + root.GetTree("t").RemoveStyle(0, 8, []string{"italic"}) + assert.Equal(t, `

ab

cd

`, root.GetTree("t").ToXML()) + return nil + }) + assert.NoError(t, err) + assert.Equal(t, 2, doc.GarbageLen()) + assert.Equal(t, 2, doc.GarbageCollect(time.MaxTicket)) + assert.Equal(t, 0, doc.GarbageLen()) + }) + + t.Run("garbage collection for tree remove style (multi clients)", func(t *testing.T) { + ctx := context.Background() + docKey := helper.TestDocKey(t) + d1 := document.New(docKey) + err := c1.Attach(ctx, d1) + assert.NoError(t, err) + + d2 := document.New(docKey) + err = c2.Attach(ctx, d2) + assert.NoError(t, err) + + err = d1.Update(func(root *json.Object, p *presence.Presence) error { + root.SetNewTree("t", &json.TreeNode{ + Type: "root", + Children: []json.TreeNode{ + {Type: "p", Children: []json.TreeNode{{Type: "text", Value: "ab"}}}, + {Type: "p", Attributes: map[string]string{"bold": "true", "italic": "true"}, Children: []json.TreeNode{{Type: "text", Value: "cd"}}}, + }, + }) + assert.Equal(t, `

ab

cd

`, root.GetTree("t").ToXML()) + + return nil + }) + assert.NoError(t, err) + + err = c1.Sync(ctx) + assert.NoError(t, err) + err = c2.Sync(ctx) + assert.NoError(t, err) + + err = d1.Update(func(root *json.Object, p *presence.Presence) error { + root.GetTree("t").RemoveStyle(4, 8, []string{"italic"}) + assert.Equal(t, `

ab

cd

`, root.GetTree("t").ToXML()) + return nil + }) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 1, d1.GarbageLen()) + assert.Equal(t, 0, d2.GarbageLen()) + + err = c1.Sync(ctx) + assert.NoError(t, err) + err = c2.Sync(ctx) + assert.NoError(t, err) + assert.Equal(t, 1, d1.GarbageLen()) + assert.Equal(t, 1, d2.GarbageLen()) + }) + t.Run("GarbageLen should return the actual number of elements garbage-collected", func(t *testing.T) { ctx := context.Background() docKey := helper.TestDocKey(t)