Skip to content

Commit

Permalink
Add read tile in MySQL storage implementation (#50)
Browse files Browse the repository at this point in the history
* Add read tile in MySQL storage implementation

* Fix misspelled `splittedIndexPath`

* Mention `parseTileLevelIndex` func doesn't return any partial tile component

* Only add the immutable `Cache-Control` header when the number of hash in the returned tile match the requested tile width
  • Loading branch information
roger2hk authored Jul 10, 2024
1 parent 1931d75 commit f592eab
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 2 deletions.
75 changes: 75 additions & 0 deletions cmd/example-mysql/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ import (
"crypto/sha256"
"database/sql"
"flag"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"

tessera "github.com/transparency-dev/trillian-tessera"
Expand Down Expand Up @@ -69,6 +72,40 @@ func main() {
}
})

http.HandleFunc("GET /tile/{level}/{index...}", func(w http.ResponseWriter, r *http.Request) {
level, index, width, err := parseTileLevelIndexWidth(r.PathValue("level"), r.PathValue("index"))
if err != nil {
w.WriteHeader(http.StatusBadRequest)
if _, werr := w.Write([]byte(fmt.Sprintf("Malformed URL: %s", err.Error()))); werr != nil {
klog.Errorf("/tile/{level}/{index...}: %v", werr)
}
return
}

tile, err := storage.ReadTile(r.Context(), level, index)
if err != nil {
if err == sql.ErrNoRows {
w.WriteHeader(http.StatusNotFound)
return
}

klog.Errorf("/tile/{level}/{index...}: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

// Only add the immutable Cache-Control header when the number of hash in the returned tile match the requested tile width.
// This ensures the response will not be cached when returning a partial tile on a full tile request.
if len(tile)/32 == int(width) {
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
}

if _, err := w.Write(tile); err != nil {
klog.Errorf("/tile/{level}/{index...}: %v", err)
return
}
})

http.HandleFunc("POST /add", func(w http.ResponseWriter, r *http.Request) {
b, err := io.ReadAll(r.Body)
if err != nil {
Expand All @@ -87,3 +124,41 @@ func main() {
klog.Exitf("ListenAndServe: %v", err)
}
}

// parseTileLevelWidthIndex takes level and index in string, validates and returns the level, index and width in uint64.
//
// Examples:
// "/tile/0/x001/x234/067" means level 0 and index 1234067 of a full tile.
// "/tile/0/x001/x234/067.p/8" means level 0, index 1234067 and width 8 of a partial tile.
func parseTileLevelIndexWidth(level, index string) (uint64, uint64, uint64, error) {
l, err := strconv.ParseUint(level, 10, 64)
if err != nil {
return 0, 0, 0, fmt.Errorf("failed to parse tile level")
}

var i, w uint64
switch indexPaths := strings.Split(index, "/"); len(indexPaths) {
// Full tile = 3
// Partial tile = 4
case 3, 4:
indexPath := strings.Join(indexPaths[0:3], "")
indexPath = strings.ReplaceAll(indexPath, "x", "")
indexPath = strings.ReplaceAll(indexPath, ".p", "")
i, err = strconv.ParseUint(indexPath, 10, 64)
if err != nil {
return 0, 0, 0, fmt.Errorf("failed to parse tile index")
}

w = 256
if len(indexPaths) == 4 {
w, err = strconv.ParseUint(indexPaths[3], 10, 64)
if err != nil {
return 0, 0, 0, fmt.Errorf("failed to parse tile index")
}
}
default:
return 0, 0, 0, fmt.Errorf("failed to parse tile index")
}

return l, i, w, nil
}
15 changes: 13 additions & 2 deletions storage/mysql/mysql.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ import (
)

const (
selectCheckpointByIDSQL = "SELECT `note` FROM `Checkpoint` WHERE `id` = ?"
replaceCheckpointSQL = "REPLACE INTO `Checkpoint` (`id`, `note`) VALUES (?, ?)"
selectCheckpointByIDSQL = "SELECT `note` FROM `Checkpoint` WHERE `id` = ?"
replaceCheckpointSQL = "REPLACE INTO `Checkpoint` (`id`, `note`) VALUES (?, ?)"
selectSubtreeByLevelAndIndexSQL = "SELECT `nodes` FROM `Subtree` WHERE `level` = ? AND `index` = ?"

checkpointID = 0
)
Expand Down Expand Up @@ -95,3 +96,13 @@ func (s *Storage) writeCheckpoint(ctx context.Context, rawCheckpoint []byte) err
// Commit the transaction.
return tx.Commit()
}

func (s *Storage) ReadTile(ctx context.Context, level, index uint64) ([]byte, error) {
row := s.db.QueryRowContext(ctx, selectSubtreeByLevelAndIndexSQL, level, index)
if err := row.Err(); err != nil {
return nil, err
}

var tile []byte
return tile, row.Scan(&tile)
}
11 changes: 11 additions & 0 deletions storage/mysql/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,14 @@ CREATE TABLE IF NOT EXISTS `Checkpoint` (
`note` MEDIUMBLOB NOT NULL,
PRIMARY KEY(`id`)
);

-- "Subtree" table is an internal tile consisting of hashes. There is one row for each internal tile, and this is updated until it is completed, at which point it is immutable.
CREATE TABLE IF NOT EXISTS `Subtree` (
-- level is the level of the tile.
`level` INT UNSIGNED NOT NULL,
-- index is the index of the tile.
`index` BIGINT UNSIGNED NOT NULL,
-- nodes stores the hashes of the leaves.
`nodes` MEDIUMBLOB NOT NULL,
PRIMARY KEY(`level`, `index`)
);

0 comments on commit f592eab

Please sign in to comment.