2
\$\begingroup\$

I've been using random UUIDs—from Rust, e.g. Uuid::new_v4()—for my primary keys in a MySQL database. I recently happened across this, which suggests that UUIDs with an appropriately positioned time component offer some performance improvements.

I created a naive UUID generator like:

fn uuid_new() -> ::uuid::Uuid {

    use ::rand::Rng;

    let now = ::std::time::SystemTime::now()
        .duration_since(::std::time::UNIX_EPOCH)
        .unwrap();

    let (secs, nans): (u64, u32) = (now.as_secs(), now.subsec_nanos());

    let rbts: [u8; 6] = ::rand::thread_rng().gen();

    let bytes = [
        // (secs >> 56) as u8,
        // (secs >> 48) as u8,
        (secs >> 40) as u8,
        (secs >> 32) as u8,
        (secs >> 24) as u8,
        (secs >> 16) as u8,
        (secs >>  8) as u8,
        (secs >>  0) as u8,
        (nans >> 24) as u8,
        (nans >> 16) as u8,
        (nans >>  8) as u8,
        (nans >>  0) as u8,
        rbts[0], rbts[1], rbts[2], rbts[3], rbts[4], rbts[5]
    ];

    ::uuid::Uuid::from_bytes(&bytes).unwrap()

}

I have no requirements for my UUIDs except speed and uniqueness. I'm truncating the first two octets of the secs u64, since they don't seem necessary for the near future.

Am I committing any faux pas here? I don't really know anything about the theory or practice of UUIDs. I'm a little bit worried since the value of get_version_num is gibberish for my UUIDs (though I am not using version number in any way).

Or are there any alternate (Rust) implementations for UUIDs with a time component?

\$\endgroup\$
3
\$\begingroup\$

No, that is not one of the five standard-compliant ways to generate a UUID. What you have written is just an obfuscated concatenation of a timestamp and a random number.

The easiest way might be to use the MySQL UUID() function instead of generating the UUID in Rust.

\$\endgroup\$
  • \$\begingroup\$ Thanks, that's good to know! But... does it matter? Intuitively, it seems like my bytes should have decent uniqueness. Also, in the Percona blog article I link, their rearranged UUIDs wouldn't technically be UUIDs anymore, would they? \$\endgroup\$ – kardeiz Jan 19 '17 at 1:55
  • 2
    \$\begingroup\$ For all practical purposes, clashes are unlikely. But standards exist for a reason, because smart people have thought them through. If you are aiming for well crafted code that behaves the way people expect, then you should follow the standard. Otherwise, don't bother claiming that it's a UUID when it isn't one. If you aren't following the standard, then you would be better off with a string that doesn't look like a UUID. \$\endgroup\$ – 200_success Jan 19 '17 at 3:16
  • \$\begingroup\$ Frankly, I'm using Uuid rather than [u8; 16] because of the built-in From[Sql]Value and To[Sql]Value traits (and easy string formatting is nice), though I realize these would be pretty trivial to implement. Using MySQL uuid() would require a second query in the insert cycle to get that value back to the client. My tables are probably small enough that I don't need to worry about performance, so I'll probably stick with Uuid::new_v4() for now. Thanks again! \$\endgroup\$ – kardeiz Jan 19 '17 at 15:32
  • \$\begingroup\$ *require a second query in the insert cycle * — Postgres has an INSERT ... RETURNING form; does MySQL not? But +1 to implementing your own wrapper around [u8; 16] and implementing the appropriate traits if you want to go forward in that direction. \$\endgroup\$ – Shepmaster Jan 19 '17 at 17:21
  • \$\begingroup\$ MySQL does not (at least not in the version I'm on)! \$\endgroup\$ – kardeiz Jan 19 '17 at 18:12
3
\$\begingroup\$

200_success has already explained the issues with the design, so I'll focus on the code:

  1. Make use of use statements instead of providing absolute paths everywhere.
  2. The type definitions when declaring secs and nans are redundant and can be inferred.
  3. I'm not a fan of creating a tuple just to destructure it immediately. Just split that into two lines.
  4. Re-creating the RNG for each call to uuid_new can be inefficient. To optimize, you could create a factory object that holds on to the RNG and reuses it.
  5. Prefer expect over unwrap. When the code eventually triggers one of those assertions, you will be thankful for the clue as to which one.
extern crate uuid;
extern crate rand;

use uuid::Uuid;
use rand::Rng;
use std::time::{SystemTime, UNIX_EPOCH};

fn uuid_new() -> Uuid {
    let now = SystemTime::now().duration_since(UNIX_EPOCH).expect("Unable to compute timestamp");

    let secs = now.as_secs();
    let nans = now.subsec_nanos();

    let rbts: [u8; 6] = rand::thread_rng().gen();

    let bytes = [(secs >> 40) as u8,
                 (secs >> 32) as u8,
                 (secs >> 24) as u8,
                 (secs >> 16) as u8,
                 (secs >>  8) as u8,
                 (secs >>  0) as u8,
                 (nans >> 24) as u8,
                 (nans >> 16) as u8,
                 (nans >>  8) as u8,
                 (nans >>  0) as u8,
                 rbts[0],
                 rbts[1],
                 rbts[2],
                 rbts[3],
                 rbts[4],
                 rbts[5]];

    Uuid::from_bytes(&bytes).expect("Unable to parse UUID bytes")
}
\$\endgroup\$
  • 1
    \$\begingroup\$ Thanks for the comments! #4 is especially helpful—I hadn't considered that it was creating a new generator each time. Re #2, I just added the signatures in case anyone wasn't familiar with the return types of those Duration methods. Re #5, I'm usually pretty careful with unwraps; from_bytes can't fail (it only errs on the arg slice len not being 16), and duration_since shouldn't fail (barring some major system time issue). But good point, you never know! \$\endgroup\$ – kardeiz Jan 19 '17 at 18:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.