Skip to content

Commit

Permalink
Snapshot IDs
Browse files Browse the repository at this point in the history
  • Loading branch information
radekg authored Feb 7, 2022
1 parent 47f8c98 commit 07dd3af
Show file tree
Hide file tree
Showing 2 changed files with 165 additions and 0 deletions.
123 changes: 123 additions & 0 deletions utils/ybdbid/snapshot_id.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package ybdbid

/**
https://yugabyte-db.slack.com/archives/CG0KQF0GG/p1644257955047349?thread_ts=1643868640.696389&cid=CG0KQF0GG
------------------------------------------------------------------------------------------------------------
Please interpret the resp.snapshot_id() as an array of 16 bytes. And nothing more. (Not md5/base64/etc.)
16 bytes. Each byte value is in: 0x00 - 0xFF range.
In C++ code the decoding only checks the string size and do memcpy:
Uuid Uuid::TryFullyDecode(const Slice& slice) {
if (slice.size() != boost::uuids::uuid::static_size()) {
return Uuid::Nil();
}
Uuid id;
memcpy(id.data(), slice.data(), boost::uuids::uuid::static_size());
return id;
}
https://yugabyte-db.slack.com/archives/CG0KQF0GG/p1644258454859919?thread_ts=1643868640.696389&cid=CG0KQF0GG
------------------------------------------------------------------------------------------------------------
Just because the Snapshot ID for non-transaction-aware-snapshot (old snapshot when
"transaction_aware=false" - not used now) was passed through the same PB field. So, in the code
"32-bytes string" = old non-transactional snapshot UUID as a string. "16 bytes" = new transactional snapshot
id in binary form. So.. two-in-one.. it's the reason of the complexity. Sorry.
As the old (non-transactional) snapshots are not used more, you can always expect the 16 bytes.
The case is only for Snapshot ID.
Namespace/Table/Tablet ID is a UUID in simple string form. No such complexities.
**/

import (
"fmt"
"strings"

"github.com/google/uuid"
)

// SnapshotID represents a parsed YugabyteDB snapshot ID.
type SnapshotID interface {
Bytes() []byte
String() string
UUID() uuid.UUID
}

type defaultSnapshotID struct {
bytes []byte
str string
uuuid uuid.UUID
}

func (id *defaultSnapshotID) Bytes() []byte {
return id.bytes
}

func (id *defaultSnapshotID) String() string {
return id.str
}

func (id *defaultSnapshotID) UUID() uuid.UUID {
return id.uuuid
}

// TryParseSnapshotIDFromBytes attempts to parse input bytes received
// from the protobuf API as a YugabyteDB snapshot ID.
func TryParseSnapshotIDFromBytes(input []byte) (SnapshotID, error) {

if len(input) != 16 {
return nil, fmt.Errorf("snapshot ID: input must be 16 bytes long")
}

aUUID := uuid.New()
if err := aUUID.UnmarshalBinary(input); err != nil {
return nil, err
}
output := &defaultSnapshotID{
bytes: make([]byte, len(input)),
str: aUUID.String(),
uuuid: aUUID,
}
copy(output.bytes, input)
return output, nil
}

// TryParseSnapshotIDFromString attempts to parse input string as a YugabyteDB
// snapshot ID. Input string must be a UUIDv4 string.
func TryParseSnapshotIDFromString(input string) (SnapshotID, error) {

switch len(input) {
// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
case 36:
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
case 36 + 9:
if strings.ToLower(input[:9]) != "urn:uuid:" {
return nil, fmt.Errorf("snapshot ID: invalid urn prefix: %q", input[:9])
}
// {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
case 36 + 2:
default:
return nil, fmt.Errorf("snapshot ID: invalid snapshot ID input")
}

// if failed, it could be a UUID string, can we parse it as such?
aUUID, err := uuid.Parse(input)
if err != nil {
// no, it's neither base64 encoded, nor looks like UUID:
return nil, fmt.Errorf("snapshot ID: input '%s' is not a valid YugabyteDB snapshot ID input", input)
}

// it parsed as UUID, we need the bytes too:
bys, err := aUUID.MarshalBinary()
if err != nil {
return nil, fmt.Errorf("snapshot ID: input '%s' is a UUID but could not be marshaled", input)
}

output := &defaultSnapshotID{
bytes: make([]byte, len(bys)),
str: aUUID.String(),
uuuid: aUUID,
}
copy(output.bytes, bys)
return output, nil
}
diff --git a/utils/ybdbid/snapshot_id_test.go b/utils/ybdbid/snapshot_id_test.go
42 changes: 42 additions & 0 deletions utils/ybdbid/snapshot_id_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package ybdbid

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestSnapshotIDParsing(t *testing.T) {

t.Run("it=parses UUID formatted input and back", func(tt *testing.T) {

validYBDBID := "dfec75ee-290e-4f3b-b965-469a0246c133"
parsed, err := TryParseSnapshotIDFromString(validYBDBID)
assert.Nil(tt, err)
assert.Equal(tt, len(parsed.Bytes()), 16)

parsedBackViaBytes, err := TryParseSnapshotIDFromBytes(parsed.Bytes())
assert.Nil(tt, err)
assert.Equal(tt, parsed.String(), parsedBackViaBytes.String())

parsedBackViaString, err := TryParseSnapshotIDFromString(parsed.String())
assert.Nil(tt, err)
assert.Equal(tt, parsed.String(), parsedBackViaString.String())

})

t.Run("it=handles non-UUID input", func(tt *testing.T) {
invalidYBDBID := "dfec75ee-290e-4f3b---b965-469a0246c133"
parsed, err := TryParseSnapshotIDFromString(invalidYBDBID)
assert.NotNil(tt, err)
assert.Nil(tt, parsed)
})

t.Run("it=handles null byte input", func(tt *testing.T) {
parsed, err := TryParseFromBytes(nil)
assert.NotNil(tt, err)
assert.Nil(tt, parsed)
})

}

0 comments on commit 07dd3af

Please sign in to comment.