Commit 748b40e8 authored by Kirill Smelkov's avatar Kirill Smelkov

.

parent ef12f1da
// Code generated by gen-set Object interface{}; DO NOT EDIT.
// Copyright (C) 2015-2019 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com>
//
// This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your
// option) any later version, as published by the Free Software Foundation.
//
// You can also Link and Combine this program with other software covered by
// the terms of any of the Free Software licenses or any of the Open Source
// Initiative approved licenses and Convey the resulting work. Corresponding
// source of such a combination shall include the source code for all other
// software used.
//
// This program is distributed WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//
// See COPYING file for full licensing terms.
// See https://www.nexedi.com/licensing for rationale and options.
package δbtree
// SetObject is a set of interface{}.
type SetObject map[interface{}]struct{}
// Add adds v to the set.
func (s SetObject) Add(v interface{}) {
s[v] = struct{}{}
}
// Has checks whether the set contains v.
func (s SetObject) Has(v interface{}) bool {
_, ok := s[v]
return ok
}
// Update adds t values to s.
func (s SetObject) Update(t SetObject) {
for v := range t {
s.Add(v)
}
}
// Elements returns all elements of set as slice.
func (s SetObject) Elements() []interface{} {
ev := make([]interface{}, len(s))
i := 0
for e := range s {
ev[i] = e
i++
}
return ev
}
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
package δbtree package δbtree
//go:generate ../../gen-set δbtree Tree *Tree zset_tree.go //go:generate ../../gen-set δbtree Tree *Tree zset_tree.go
//go:generate ../../gen-set δbtree Object interface{} zset_object.go
import ( import (
"fmt" "fmt"
...@@ -39,17 +40,29 @@ type Bucket = btree.LOBucket ...@@ -39,17 +40,29 @@ type Bucket = btree.LOBucket
// //
// PathSet is not safe for concurrent access. // PathSet is not safe for concurrent access.
type PathSet struct { type PathSet struct {
// residency index which maps BTree|Bucket to top tree element. // residency index: BTree|Bucket to top tree element.
residencyIdx map[zodb.Oid]SetTree // oid -> {} roots residencyIdx map[zodb.Oid]SetTree // oid -> {} roots
// master index: tree root -> {} masters
masterIdx map[*Tree]SetObject
}
// NewPathSet creates new empty PathSet.
func NewPathSet() *PathSet {
return &PathSet{
residencyIdx: make(map[zodb.Oid]SetTree),
masterIdx: make(map[*Tree]SetObject),
}
} }
// Add adds path to collection. // Add adds path to collection and associates path root with master.
// //
// path[0] signifies a root. // path[0] signifies a root.
// All path elemens must be Tree except last one which must be Bucket. // All path elemens must be Tree except last one which must be Bucket.
// A root can be associated with several masters (each provided on different Add call)
// //
// XXX catch cycles on add? // XXX catch cycles on add?
func (m *PathSet) Add(path []zodb.IPersistent) { func (m *PathSet) Add(master interface{}, path []zodb.IPersistent) {
l := len(path) l := len(path)
if l == 0 { if l == 0 {
panic("empty path") panic("empty path")
...@@ -81,17 +94,24 @@ func (m *PathSet) Add(path []zodb.IPersistent) { ...@@ -81,17 +94,24 @@ func (m *PathSet) Add(path []zodb.IPersistent) {
} }
nodeRoots.Add(root) nodeRoots.Add(root)
} }
masterSet, ok := m.masterIdx[root]
if !ok {
masterSet = SetObject{}
m.masterIdx[root] = masterSet
}
masterSet.Add(master)
} }
// Invalidates returns which trees are invalidated by changed objects. // Invalidates returns which masters are invalidated by changed objects.
// //
// returned are: roots of changed trees. // returned are: roots of changed trees.
// //
// XXX stub. // XXX stub.
// //
// TODO toposort changev wrt nodes ordering (defined by traversed paths) and do δbtree top-down. // TODO toposort changev wrt nodes ordering (defined by traversed paths) and do δbtree top-down.
func (m *PathSet) Invalidates(changev []zodb.Oid) SetTree { func (m *PathSet) Invalidates(changev []zodb.Oid) (masters SetObject) {
toinvalidate := make(SetTree) masters = SetObject{}
for _, oid := range changev { for _, oid := range changev {
roots, ok := m.residencyIdx[oid] roots, ok := m.residencyIdx[oid]
...@@ -99,8 +119,10 @@ func (m *PathSet) Invalidates(changev []zodb.Oid) SetTree { ...@@ -99,8 +119,10 @@ func (m *PathSet) Invalidates(changev []zodb.Oid) SetTree {
continue // we don't know this oid continue // we don't know this oid
} }
toinvalidate.Update(roots) for root := range roots {
masters.Update(m.masterIdx[root])
}
} }
return toinvalidate return masters
} }
...@@ -511,3 +511,21 @@ func (u *zrevTabUnregister) AfterCompletion(txn transaction.Transaction) { ...@@ -511,3 +511,21 @@ func (u *zrevTabUnregister) AfterCompletion(txn transaction.Transaction) {
} }
} }
*/ */
// tidmax returns latest revision.
func tidmax(a, b zodb.Tid) zodb.Tid {
if a > b {
return a
} else {
return b
}
}
// tidmin returns earliest revision.
func tidmin(a, b zodb.Tid) zodb.Tid {
if a < b {
return a
} else {
return b
}
}
...@@ -431,6 +431,7 @@ type Root struct { ...@@ -431,6 +431,7 @@ type Root struct {
// /(head|<rev>)/ - served by Head. // /(head|<rev>)/ - served by Head.
type Head struct { type Head struct {
nodefs.Node nodefs.Node
rev zodb.Tid // 0 for head/, !0 for @<rev>/ rev zodb.Tid // 0 for head/, !0 for @<rev>/
bfdir *BigFileDir // bigfile/ bfdir *BigFileDir // bigfile/
// at - served by .readAt // at - served by .readAt
...@@ -457,11 +458,13 @@ type BigFileDir struct { ...@@ -457,11 +458,13 @@ type BigFileDir struct {
head *Head // parent head/ or @<rev>/ head *Head // parent head/ or @<rev>/
// {} oid -> <bigfileX> // {} oid -> <bigfileX>
mu sync.Mutex fileMu sync.Mutex
fileTab map[zodb.Oid]*BigFile fileTab map[zodb.Oid]*BigFile
// visited BTree nodes -> which file + ordering for toposort on δbtree // visited BTree nodes of all BigFiles
blktabLoaded *δbtree.PathSet // -> which file + ordering for toposort on δbtree
indexMu sync.Mutex
indexLooked *δbtree.PathSet // XXX naming
} }
// /(head|<rev>)/bigfile/<bigfileX> - served by BigFile. // /(head|<rev>)/bigfile/<bigfileX> - served by BigFile.
...@@ -599,7 +602,13 @@ func (root *Root) zδhandle1(zevent zodb.CommitEvent) { ...@@ -599,7 +602,13 @@ func (root *Root) zδhandle1(zevent zodb.CommitEvent) {
zhead := root.head.zconn zhead := root.head.zconn
bfdir := root.head.bfdir bfdir := root.head.bfdir
toinvalidate := map[*BigFile]SetI64{} // {} file -> set(#blk) // fileInvalidate describes invalidations for one file
type fileInvalidate struct {
blkmap SetI64 // changed blocks
size bool // whether to invalidate file sise
}
toinvalidate := map[*BigFile]*fileInvalidate{} // {} file -> set(#blk), sizeChanged
btreeChangev := []zodb.Oid{} // oids changing BTree|Bucket
fmt.Printf("\n\n\n") fmt.Printf("\n\n\n")
...@@ -619,10 +628,10 @@ func (root *Root) zδhandle1(zevent zodb.CommitEvent) { ...@@ -619,10 +628,10 @@ func (root *Root) zδhandle1(zevent zodb.CommitEvent) {
continue // object not related to any bigfile continue // object not related to any bigfile
case *btree.LOBTree: case *btree.LOBTree:
// XXX -> δBTree btreeChangev = append(btreeChangev, obj.POid())
case *btree.LOBucket: case *btree.LOBucket:
// XXX -> δBTree btreeChangev = append(btreeChangev, obj.POid())
case zBlk: // ZBlk* case zBlk: // ZBlk*
// blkBoundTo locking: no other bindZFile are running, // blkBoundTo locking: no other bindZFile are running,
...@@ -640,12 +649,12 @@ func (root *Root) zδhandle1(zevent zodb.CommitEvent) { ...@@ -640,12 +649,12 @@ func (root *Root) zδhandle1(zevent zodb.CommitEvent) {
continue continue
} }
blkmap, ok := toinvalidate[file] finv, ok := toinvalidate[file]
if !ok { if !ok {
blkmap = SetI64{} finv = &fileInvalidate{blkmap: SetI64{}}
toinvalidate[file] = blkmap toinvalidate[file] = finv
} }
blkmap.Update(objBlk) finv.blkmap.Update(objBlk)
} }
case *ZBigFile: case *ZBigFile:
...@@ -659,15 +668,31 @@ func (root *Root) zδhandle1(zevent zodb.CommitEvent) { ...@@ -659,15 +668,31 @@ func (root *Root) zδhandle1(zevent zodb.CommitEvent) {
runtime.KeepAlive(obj) runtime.KeepAlive(obj)
} }
// find out which files need to be invalidated due to index change
// XXX no indexMu lock needed because head is Locked
// XXX stub -> TODO full δbtree
fmt.Printf("\nbtreeChangev: %v\n", btreeChangev)
xfiles := bfdir.indexLooked.Invalidates(btreeChangev)
fmt.Printf("xfiles: %v\n", xfiles)
for xfile := range xfiles {
file := xfile.(*BigFile)
finv, ok := toinvalidate[file]
if !ok {
finv = &fileInvalidate{} // XXX init blkmap?
toinvalidate[file] = finv
}
finv.size = true
}
fmt.Printf("\n\nzδhandle: toinvalidate (#%d):\n", len(toinvalidate)) fmt.Printf("\n\nzδhandle: toinvalidate (#%d):\n", len(toinvalidate))
for file := range toinvalidate { for file := range toinvalidate {
fmt.Printf("\t- %s\n", file.zbf.POid()) fmt.Printf("\t- %s\n", file.zbf.POid())
} }
wg, ctx := errgroup.WithContext(context.TODO()) wg, ctx := errgroup.WithContext(context.TODO())
for file, blkmap := range toinvalidate { for file, finv := range toinvalidate {
file := file file := file
for blk := range blkmap { for blk := range finv.blkmap {
blk := blk blk := blk
wg.Go(func() error { wg.Go(func() error {
return file.invalidateBlk(ctx, blk) return file.invalidateBlk(ctx, blk)
...@@ -675,10 +700,12 @@ func (root *Root) zδhandle1(zevent zodb.CommitEvent) { ...@@ -675,10 +700,12 @@ func (root *Root) zδhandle1(zevent zodb.CommitEvent) {
} }
// invalidate kernel cache for attributes // invalidate kernel cache for attributes
// XXX we need to do it only if we see topoligy (i.e. btree) change // we need to do it only if we see topoligy (i.e. btree) change
wg.Go(func() error { if finv.size {
return file.invalidateAttr() wg.Go(func() error {
}) return file.invalidateAttr()
})
}
} }
err := wg.Wait() err := wg.Wait()
if err != nil { if err != nil {
...@@ -688,12 +715,11 @@ func (root *Root) zδhandle1(zevent zodb.CommitEvent) { ...@@ -688,12 +715,11 @@ func (root *Root) zδhandle1(zevent zodb.CommitEvent) {
// resync .zhead to zevent.tid // resync .zhead to zevent.tid
// XXX -> Head.Resync() ? // XXX -> Head.Resync() ?
// 1. deactivate all ZBigFile (we keep them activated during whole txn) // 1. deactivate changed ZBigFile (we keep all ZBigFiles activated during whole txn)
// XXX dir.mu locking (not needed bcause zconnMu locked) // XXX dir.fileMu locking (not needed bcause zconnMu locked)
for _, file := range bfdir.fileTab { for file := range toinvalidate {
file.zbf.PDeactivate() file.zbf.PDeactivate()
// XXX we need to reread size only for files in toinvalidate file.zbfSize = -1
file.zbfSize = -1 // just in case
} }
// 2. abort old and resync to new txn/at // 2. abort old and resync to new txn/at
...@@ -702,10 +728,10 @@ func (root *Root) zδhandle1(zevent zodb.CommitEvent) { ...@@ -702,10 +728,10 @@ func (root *Root) zδhandle1(zevent zodb.CommitEvent) {
zhead.Resync(txn, zevent.Tid) zhead.Resync(txn, zevent.Tid)
zhead.txnCtx = ctx zhead.txnCtx = ctx
// 3. reactivate/restat all ZBigFile // 3. reactivate/restat invalidated ZBigFile
// XXX -> parallel? // XXX -> parallel?
// XXX locking // XXX locking
for _, file := range bfdir.fileTab { for file := range toinvalidate {
err := file.zbf.PActivate(ctx) err := file.zbf.PActivate(ctx)
if err != nil { if err != nil {
panic(err) // XXX panic(err) // XXX
...@@ -859,9 +885,9 @@ func (bfdir *BigFileDir) lookup(out *fuse.Attr, name string, fctx *fuse.Context) ...@@ -859,9 +885,9 @@ func (bfdir *BigFileDir) lookup(out *fuse.Attr, name string, fctx *fuse.Context)
}() }()
// check to see if dir(oid) is already there // check to see if dir(oid) is already there
bfdir.mu.Lock() bfdir.fileMu.Lock()
f, already := bfdir.fileTab[oid] f, already := bfdir.fileTab[oid]
bfdir.mu.Unlock() bfdir.fileMu.Unlock()
if already { if already {
return f, nil return f, nil
...@@ -874,19 +900,19 @@ func (bfdir *BigFileDir) lookup(out *fuse.Attr, name string, fctx *fuse.Context) ...@@ -874,19 +900,19 @@ func (bfdir *BigFileDir) lookup(out *fuse.Attr, name string, fctx *fuse.Context)
} }
// relock bfdir and either register f or, if the file was maybe // relock bfdir and either register f or, if the file was maybe
// simultanously created while we were not holding bfdir.mu, return that. // simultanously created while we were not holding bfdir.fileMu, return that.
bfdir.mu.Lock() bfdir.fileMu.Lock()
f2, already := bfdir.fileTab[oid] f2, already := bfdir.fileTab[oid]
if already { if already {
bfdir.mu.Unlock() bfdir.fileMu.Unlock()
f.Close() f.Close()
return f2, nil return f2, nil
} }
bfdir.fileTab[oid] = f bfdir.fileTab[oid] = f
bfdir.mu.Unlock() bfdir.fileMu.Unlock()
// mkfile takes filesystem treeLock - do it outside bfdir.mu // mkfile takes filesystem treeLock - do it outside bfdir.fileMu
mkfile(bfdir, name, f) mkfile(bfdir, name, f)
return f, nil return f, nil
...@@ -958,6 +984,7 @@ func (root *Root) mkdir(name string, fctx *fuse.Context) (_ *nodefs.Inode, err e ...@@ -958,6 +984,7 @@ func (root *Root) mkdir(name string, fctx *fuse.Context) (_ *nodefs.Inode, err e
Node: newDefaultNode(), Node: newDefaultNode(),
head: revDir, head: revDir,
fileTab: make(map[zodb.Oid]*BigFile), fileTab: make(map[zodb.Oid]*BigFile),
// indexLooked = nil for @revX/
} }
revDir.bfdir = bfdir revDir.bfdir = bfdir
...@@ -1156,19 +1183,19 @@ func (f *BigFile) readBlk(ctx context.Context, blk int64, dest []byte) error { ...@@ -1156,19 +1183,19 @@ func (f *BigFile) readBlk(ctx context.Context, blk int64, dest []byte) error {
// noone was loading - we became reponsible to load this block // noone was loading - we became reponsible to load this block
zbf := f.zbf zbf := f.zbf
blkdata, treepath, blkpathRevMax, err := zbf.LoadBlk(ctx, blk) blkdata, treepath, pathRevMax, err := zbf.LoadBlk(ctx, blk)
loading.blkdata = blkdata loading.blkdata = blkdata
loading.err = err loading.err = err
close(loading.ready) close(loading.ready)
// XXX locking bfdir := f.head.bfdir
x.blktabVisited.Add(treepath) bfdir.indexMu.Lock() // XXX locking correct?
bfdir.indexLooked.Add(f, treepath) // XXX needed only for head/
bfdir.indexMu.Unlock()
// XXX before loading.ready? // XXX before loading.ready?
blkrevmax, _ := f.δFtail.LastRevOf(blk, zbf.PJar().At()) blkrevmax, _ := f.δFtail.LastRevOf(blk, zbf.PJar().At())
if blkpathRevMax < blkrevmax { blkrevmax = tidmin(blkrevmax, pathRevMax)
blkrevmax = blkpathRevMax
}
/* /*
// XXX remmapping - only if head.rev == 0 // XXX remmapping - only if head.rev == 0
...@@ -1343,9 +1370,10 @@ func main() { ...@@ -1343,9 +1370,10 @@ func main() {
zconn: zhead, zconn: zhead,
} }
bfdir := &BigFileDir{ bfdir := &BigFileDir{
Node: newDefaultNode(), Node: newDefaultNode(),
head: head, head: head,
fileTab: make(map[zodb.Oid]*BigFile), fileTab: make(map[zodb.Oid]*BigFile),
indexLooked: δbtree.NewPathSet(),
} }
head.bfdir = bfdir head.bfdir = bfdir
......
...@@ -61,7 +61,9 @@ type zBlk interface { ...@@ -61,7 +61,9 @@ type zBlk interface {
// //
// If returned data size is less than the block size of containing ZBigFile, // If returned data size is less than the block size of containing ZBigFile,
// the block trailing is assumed to be trailing \0. // the block trailing is assumed to be trailing \0.
loadBlkData(ctx context.Context) ([]byte, error) //
// returns data and revision of ZBlk.
loadBlkData(ctx context.Context) (data []byte, rev zodb.Tid, _ error)
// bindZFile associates ZBlk as being used by zfile to store block #blk. // bindZFile associates ZBlk as being used by zfile to store block #blk.
// //
...@@ -155,16 +157,16 @@ func (zb *zBlk0State) PySetState(pystate interface{}) error { ...@@ -155,16 +157,16 @@ func (zb *zBlk0State) PySetState(pystate interface{}) error {
return nil return nil
} }
func (zb *ZBlk0) loadBlkData(ctx context.Context) ([]byte, error) { func (zb *ZBlk0) loadBlkData(ctx context.Context) ([]byte, zodb.Tid, error) {
// XXX err ctx // XXX err ctx
err := zb.PActivate(ctx) err := zb.PActivate(ctx)
if err != nil { if err != nil {
return nil, err return nil, 0, err
} }
defer zb.PDeactivate() defer zb.PDeactivate()
return mem.Bytes(zb.blkdata), nil return mem.Bytes(zb.blkdata), zb.PSerial(), nil
} }
// ---- ZBlk1 --- // ---- ZBlk1 ---
...@@ -227,12 +229,12 @@ func (zb *zBlk1State) PySetState(pystate interface{}) error { ...@@ -227,12 +229,12 @@ func (zb *zBlk1State) PySetState(pystate interface{}) error {
return nil return nil
} }
func (zb *ZBlk1) loadBlkData(ctx context.Context) ([]byte, error) { func (zb *ZBlk1) loadBlkData(ctx context.Context) ([]byte, zodb.Tid, error) {
// XXX errctx // XXX errctx
err := zb.PActivate(ctx) err := zb.PActivate(ctx)
if err != nil { if err != nil {
return nil, err return nil, 0, err
} }
defer zb.PDeactivate() defer zb.PDeactivate()
...@@ -337,12 +339,12 @@ func (zb *ZBlk1) loadBlkData(ctx context.Context) ([]byte, error) { ...@@ -337,12 +339,12 @@ func (zb *ZBlk1) loadBlkData(ctx context.Context) ([]byte, error) {
err = wg.Wait() err = wg.Wait()
if err != nil { if err != nil {
return nil, err // XXX err ctx return nil, 0, err // XXX err ctx
} }
// empty .chunktab -> ø // empty .chunktab -> ø
if len(chunktab) == 0 { if len(chunktab) == 0 {
return nil, nil return nil, 0, nil
} }
// glue all chunks from chunktab // glue all chunks from chunktab
...@@ -372,16 +374,16 @@ func (zb *ZBlk1) loadBlkData(ctx context.Context) ([]byte, error) { ...@@ -372,16 +374,16 @@ func (zb *ZBlk1) loadBlkData(ctx context.Context) ([]byte, error) {
for _, start := range offv[:len(offv)-1] { for _, start := range offv[:len(offv)-1] {
chunk := chunktab[start] chunk := chunktab[start]
if !(start >= stop) { // verify chunks don't overlap if !(start >= stop) { // verify chunks don't overlap
return nil, fmt.Errorf("!(start >= stop)") // XXX return nil, 0, fmt.Errorf("!(start >= stop)") // XXX
} }
if !(start + int32(len(chunk.data)) <= int32(len(blkdata))) { // XXX overflow? if !(start + int32(len(chunk.data)) <= int32(len(blkdata))) { // XXX overflow?
return nil, fmt.Errorf("blkdata overrun") // XXX return nil, 0, fmt.Errorf("blkdata overrun") // XXX
} }
stop = start + int32(len(chunk.data)) // XXX overflow? stop = start + int32(len(chunk.data)) // XXX overflow?
copy(blkdata[start:], chunk.data) copy(blkdata[start:], chunk.data)
} }
return blkdata, nil return blkdata, zb.PSerial(), nil
} }
...@@ -443,7 +445,7 @@ func (bf *zBigFileState) PySetState(pystate interface{}) (err error) { ...@@ -443,7 +445,7 @@ func (bf *zBigFileState) PySetState(pystate interface{}) (err error) {
// it also returns: // it also returns:
// //
// - BTree path in .blktab for loaded block, // - BTree path in .blktab for loaded block,
// - maximum revision of nodes in the BTree path. // - max(_.serial for _ in ZBlk(#blk), all BTree/Bucket that lead to ZBlk)
// //
// XXX better load into user-provided buf? mem.Buf? // XXX better load into user-provided buf? mem.Buf?
func (bf *ZBigFile) LoadBlk(ctx context.Context, blk int64) (_ []byte, treePath []zodb.IPersistent, pathRevMax zodb.Tid, err error) { func (bf *ZBigFile) LoadBlk(ctx context.Context, blk int64) (_ []byte, treePath []zodb.IPersistent, pathRevMax zodb.Tid, err error) {
...@@ -458,10 +460,7 @@ func (bf *ZBigFile) LoadBlk(ctx context.Context, blk int64) (_ []byte, treePath ...@@ -458,10 +460,7 @@ func (bf *ZBigFile) LoadBlk(ctx context.Context, blk int64) (_ []byte, treePath
pathRevMax = 0 pathRevMax = 0
xzblk, ok, err := bf.blktab.GetTo(ctx, blk, func(node zodb.IPersistent) { xzblk, ok, err := bf.blktab.GetTo(ctx, blk, func(node zodb.IPersistent) {
treePath = append(treePath, node) treePath = append(treePath, node)
rev := node.PSerial() pathRevMax = tidmax(pathRevMax, node.PSerial())
if rev > pathRevMax {
pathRevMax = rev
}
}) })
if err != nil { if err != nil {
return nil, nil, 0, err return nil, nil, 0, err
...@@ -475,10 +474,11 @@ func (bf *ZBigFile) LoadBlk(ctx context.Context, blk int64) (_ []byte, treePath ...@@ -475,10 +474,11 @@ func (bf *ZBigFile) LoadBlk(ctx context.Context, blk int64) (_ []byte, treePath
return nil, nil, 0, fmt.Errorf("expect ZBlk*; got %s", typeOf(xzblk)) return nil, nil, 0, fmt.Errorf("expect ZBlk*; got %s", typeOf(xzblk))
} }
blkdata, err := zblk.loadBlkData(ctx) blkdata, blkrev, err := zblk.loadBlkData(ctx)
if err != nil { if err != nil {
return nil, nil, 0, err return nil, nil, 0, err
} }
pathRevMax = tidmax(pathRevMax, blkrev)
l := int64(len(blkdata)) l := int64(len(blkdata))
if l > bf.blksize { if l > bf.blksize {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment