gobucketboltdb

Golang BoltDB Delete Key Seemingly Not Working


CentOS 7, Github boltdb/bolt version 1.3.1, go version go1.17.7 linux/amd64

This issue may go to a misunderstanding of how BoltDB works, or maybe I have a bug, or maybe there is an issue. I've used BoltDB before, and have had very good results. Though, I didn't explicly look for this issue. What I'm seeing is that I try to delete a key from a bucket, and the key and its value are deleted in the active db.Update, but it's still there after that db.Update is ended. Looking for any explanation of what might be going on. Seems like this functionality couldn't possibly be broken.

I am using a BoltDB bucket for storing a temporary token associated with an email address for creating a new account. Want to be tidy and clean up old data right away (expired tokens, misused tokens, etc). Pretty standard stuff. The structure for the temporary token is (the key is the temporary token, a 10 digit random character string):

(Temporary Token is the Bucket key)

type tempTokenStruct struct {

EmailAddress        string   `json:"emailaddress"`        // Email Address to be changed 

TokenExpiryTime     int64    `json:"tokenexpirytime"`     // Expiry Time for token in Epoch time

}

The user enters an email address in a web form and hits 'submit'. That creates a call to the REST service that creates an entry in the temporary token table, like:

"BpLnfgDsc2" => foo@bar.com, 1645650084

The service emails a URL that has the temporary token embedded, and that link takes the user to a form that allows them to put in their email address (again to verify) and new password (twice). Hitting Submit then results in the following code being called from within a web handler:

func checkTokenValid(emailAddress string, tempToken string) error {
var tempTokenData tempTokenStruct
var tempTokenBytes []byte

tempTokenBytes = []byte(tempToken)

db, err := bolt.Open(USER_DB_NAME, 0644, nil)

if err != nil {
    return err
}

defer db.Close()

err = db.Update(func(tx *bolt.Tx) error {
    tempTokenBucket := tx.Bucket([]byte("temptokens"))

    // The bucket hasn't been created, so there are no stored tokens
    if tempTokenBucket == nil {
        return errors.New("Not Authorized (1): Please request a new password new/change email from the login page.")
    }

    // There is no matching token stored in the bucket, so this is an invalid request
    tempTokenJSON := tempTokenBucket.Get(tempTokenBytes)

    //[I've put a printf here: A]

    if tempTokenJSON == nil {
        return errors.New("Not Authorized (2): Please request a new password new/change email from the login page.")
    }

    jsonConvertErr := json.Unmarshal(tempTokenJSON, &tempTokenData)

    if jsonConvertErr != nil {
        tempTokenBucket.Delete(tempTokenBytes)
        return errors.New("Not Authorized (3): Please request a new password new/change email from the login page.")
    }

    // Check to see if the time is expired, if so, remove the key and indicate error
    if tempTokenData.TokenExpiryTime < time.Now().Unix() {
        tempTokenBucket.Delete(tempTokenBytes)

        //[I've put a printf here: B]

        return errors.New("Not Authorized (4): Please request a new password new/change email from the login page.")
    }

    // Check to see if the email addresses match
    if emailAddress != tempTokenData.EmailAddress {
        tempTokenBucket.Delete(tempTokenBytes)
        return errors.New("Not Authorized (5): Please request a new password new/change email from the login page.")
    }

    tempTokenBucket.Delete(tempTokenBytes)
    return nil
})

// This is test code to see if the key was in fact deleted
db.Update(func(tx *bolt.Tx) error {
    tempTokenBucket := tx.Bucket([]byte("temptokens"))
    tempTokenJSON := tempTokenBucket.Get(tempTokenBytes)

    // [I've put a printf here: C]

    return nil
})

return err
}

I'm testing with a timed-out token (4), so the idea is that when it encounters that timed out token, it wants to delete this now invalid token from the bucket.

At the A location, it prints: First Get call token BpLnfgDsc2 is {"emailaddress":"foo@bar.com","tokenexpirytime":1645650084}

At the B location I put code in that does a .Get, it prints out (looks to be deleted): Before the DB Close (4), after deleting, token BpLnfgDsc2 is

At the C location, it prints (looks to be back): After the DB Close, token BpLnfgDsc2 is {"emailaddress":"foo@bar.com","tokenexpirytime":1645650084}

There are no errors returned for anything. I've repeated this many times, putting fmt.Printfs everywhere to see what's going on. The results are the same, the key doesn't seem to be getting deleted. After this sits, I 'vi -b' the DB file, and the key, value is still there. Running after it sits, it still sees the key value there. I'm confused, and any pointers will be appreciated.

Update: The basic bolt functionality of Put/Get/Delete/Get works as per this test code (should be obvious):

package main

import "fmt"
import "encoding/json"
import "github.com/boltdb/bolt"

type tempTokenStruct struct {
EmailAddress        string   `json:"emailaddress"`        // Email Address to be changed (Temporary Token is the DB key)
TokenExpiryTime     int64    `json:"tokenexpirytime"`     // Expiry Time for token in Epoch time
}


func main() {
    var tempToken tempTokenStruct

    tempToken.EmailAddress = "foo@bar.com"
    tempToken.TokenExpiryTime = 1234567890

    tempTokenDataJSON, jsonMarshalError := json.Marshal(tempToken)

    if jsonMarshalError != nil {
            fmt.Printf("JSON Marshal Error: %s\n", jsonMarshalError.Error())
            return
    }

    tempTokenKey := []byte("foo")

db, err := bolt.Open("test.db", 0644, nil)

if err != nil {
            fmt.Printf("Error opening Database\n")
    return
}

defer db.Close()

// Put a key in the table
err = db.Update(func(tx *bolt.Tx) error {
    tempTokenBucket, err := tx.CreateBucketIfNotExists([]byte("temptokens"))

    if err != nil {
        return err
    }

    dbPutError := tempTokenBucket.Put(tempTokenKey, []byte(tempTokenDataJSON))

    return dbPutError
})

    if err != nil {
            fmt.Printf("Error putting key value pair into table: %s\n", err.Error())
    }

    // Check if the key/value is there after putting it in
err = db.Update(func(tx *bolt.Tx) error {
    tempTokenBucket, err := tx.CreateBucketIfNotExists([]byte("temptokens"))

    if err != nil {
        return err
    }

    valueGet := tempTokenBucket.Get(tempTokenKey)

            fmt.Printf("Value for Token: \"%s\" is \"%s\" just after putting it in there\n", tempTokenKey, valueGet)

    return nil
})

    // Delete that key from the table
err = db.Update(func(tx *bolt.Tx) error {
    tempTokenBucket, err := tx.CreateBucketIfNotExists([]byte("temptokens"))

    if err != nil {
        return err
    }

    dbDeleteError := tempTokenBucket.Delete(tempTokenKey)

    return dbDeleteError
})

    if err != nil {
            fmt.Printf("Error Deleting key from bucket: %s\n", err.Error())
    }

    // Check if the key/value is there after deleting it
err = db.Update(func(tx *bolt.Tx) error {
    tempTokenBucket, err := tx.CreateBucketIfNotExists([]byte("temptokens"))

    if err != nil {
        return err
    }

    valueGet := tempTokenBucket.Get(tempTokenKey)

            fmt.Printf("Value for Token: \"%s\" is \"%s\" after the delete\n", tempTokenKey, valueGet)

    return nil
})

    if err != nil {
            fmt.Printf("Error getting key from table: %s\n", err.Error())
    }
}

Prints out:

Value for Token: "foo" is "{"emailaddress":"foo@bar.com","tokenexpirytime":1234567890}" just after putting it in there

Value for Token: "foo" is "" after the delete

So, not sure why the other code doesn't work. Almost as if the delete is using a different key, but the key is the same across the other code.


Solution

  • I believe that the behaviour of db.Update with a non-nil return value is the confusion here. As per the docs

    Inside the closure, you have a consistent view of the database. You commit the transaction by returning nil at the end. You can also rollback the transaction at any point by returning an error.

    You are returning an error with:

    return errors.New("Not Authorized (4): Please request a new password new/change email from the login page.")
    

    This means that all operations within that db.Update( are rolled back. This can be replicated in your simple example with a small change (return fmt.Errorf("RETURNING ERROR HERE")):

    package main
    
    import "fmt"
    import "encoding/json"
    import "github.com/boltdb/bolt"
    
    type tempTokenStruct struct {
        EmailAddress    string `json:"emailaddress"`    // Email Address to be changed (Temporary Token is the DB key)
        TokenExpiryTime int64  `json:"tokenexpirytime"` // Expiry Time for token in Epoch time
    }
    
    func main() {
        var tempToken tempTokenStruct
        tempToken.EmailAddress = "foo@bar.com"
        tempToken.TokenExpiryTime = 1234567890
    
        tempTokenDataJSON, jsonMarshalError := json.Marshal(tempToken)
        if jsonMarshalError != nil {
            fmt.Printf("JSON Marshal Error: %s\n", jsonMarshalError.Error())
            return
        }
        tempTokenKey := []byte("foo")
        db, err := bolt.Open("test.db", 0644, nil)
        if err != nil {
            fmt.Printf("Error opening Database\n")
            return
        }
        defer db.Close()
    
        // Put a key in the table
        err = db.Update(func(tx *bolt.Tx) error {
            tempTokenBucket, err := tx.CreateBucketIfNotExists([]byte("temptokens"))
    
            if err != nil {
                return err
            }
    
            dbPutError := tempTokenBucket.Put(tempTokenKey, []byte(tempTokenDataJSON))
    
            return dbPutError
        })
    
        if err != nil {
            fmt.Printf("Error putting key value pair into table: %s\n", err.Error())
        }
    
        // Check if the key/value is there after putting it in
        err = db.Update(func(tx *bolt.Tx) error {
            tempTokenBucket, err := tx.CreateBucketIfNotExists([]byte("temptokens"))
    
            if err != nil {
                return err
            }
    
            valueGet := tempTokenBucket.Get(tempTokenKey)
    
            fmt.Printf("Value for Token: \"%s\" is \"%s\" just after putting it in there\n", tempTokenKey, valueGet)
    
            return nil
        })
    
        // Delete that key from the table
        err = db.Update(func(tx *bolt.Tx) error {
            tempTokenBucket, err := tx.CreateBucketIfNotExists([]byte("temptokens"))
    
            if err != nil {
                return err
            }
            tempTokenBucket.Delete(tempTokenKey)
            return fmt.Errorf("RETURNING ERROR HERE")  // CHANGED HERE
        })
    
        if err != nil {
            fmt.Printf("Error Deleting key from bucket: %s\n", err.Error())
        }
    
        // Check if the key/value is there after deleting it
        err = db.Update(func(tx *bolt.Tx) error {
            tempTokenBucket, err := tx.CreateBucketIfNotExists([]byte("temptokens"))
            if err != nil {
                return err
            }
            valueGet := tempTokenBucket.Get(tempTokenKey)
            fmt.Printf("Value for Token: \"%s\" is \"%s\" after the delete\n", tempTokenKey, valueGet)
            return nil
        })
    
        if err != nil {
            fmt.Printf("Error getting key from table: %s\n", err.Error())
        }
    }
    

    The output is now:

    Value for Token: "foo" is "{"emailaddress":"foo@bar.com","tokenexpirytime":1234567890}" just after putting it in there
    Error Deleting key from bucket: RETURNING ERROR HERE
    Value for Token: "foo" is "{"emailaddress":"foo@bar.com","tokenexpirytime":1234567890}" after the delete
    

    This appears to match what you are seeing in your main code. The fix is relatively simple - don't return an error if you want changes to be committed.