Skip to content

Commit

Permalink
fixed on image cache
Browse files Browse the repository at this point in the history
Signed-off-by: p4u <[email protected]>
  • Loading branch information
p4u committed Mar 6, 2024
1 parent 8a35091 commit c57f1f2
Show file tree
Hide file tree
Showing 5 changed files with 69 additions and 35 deletions.
4 changes: 2 additions & 2 deletions image.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ func (v *vocdoniHandler) imagesHandler(msg *apirest.APIdata, ctx *httprouter.HTT
// check if the election is finished and if so, send the final results as a static PNG
pngResults := v.db.FinalResultsPNG(electionID)
if pngResults != nil {
log.Warnw("image recovery, found final results PNG!", "electionID", electionID, "imgID", id)
// for future requests, add the image to the cache with the given id
imageframe.AddImageToCacheWithID(id, pngResults)
return imageResponse(ctx, pngResults)
Expand All @@ -70,8 +71,7 @@ func (v *vocdoniHandler) imagesHandler(msg *apirest.APIdata, ctx *httprouter.HTT
if err != nil {
return errorImageResponse(ctx, fmt.Errorf("failed to build landing: %w", err))
}
// for future requests, add the image to the cache with the given id
imageframe.AddImageToCacheWithID(id, imageframe.FromCache(png))
log.Warnw("image recovery, built landing PNG", "electionID", electionID, "imgID", id)
return imageResponse(ctx, imageframe.FromCache(png))
}

Expand Down
40 changes: 25 additions & 15 deletions imageframe/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,45 @@ import (

"github.com/zeebo/blake3"
"go.vocdoni.io/dvote/api"
"go.vocdoni.io/dvote/log"
)

// cacheElectionID returns a unique identifier cache key, for the election.
// generateElectionCacheKey returns a unique identifier cache key, for the election.
// The cache key is based on the electionID, voteCount and finalResults.
func cacheElectionID(election *api.Election) string {
func generateElectionCacheKey(election *api.Election, imageType int) string {
if election == nil {
return ""
}
return fmt.Sprintf("%s_%d-%d", election.ElectionID.String(), election.VoteCount, func() int {
if election.FinalResults {
return 1
}
return 0
}())
switch imageType {
case imageTypeResults:
return fmt.Sprintf("%s_%d-%d%d", election.ElectionID.String(), election.VoteCount, func() int {
if election.FinalResults {
return 1
}
return 0
}(), imageType)
case imageTypeQuestion:
return fmt.Sprintf("%s_%d", election.ElectionID.String(), imageType)
default:
log.Errorw(fmt.Errorf("unknown image type %d", imageType), "cacheElectionID")
// fallback
return fmt.Sprintf("%s_%d", election.ElectionID.String(), imageType)
}
}

// addElectionImageToCache adds an image to the LRU cache.
// cacheElectionImage adds an image to the LRU cache.
// Returns the cache key.
// If electionID is nil, the image is not associated with any election.
func addElectionImageToCache(data []byte, election *api.Election) string {
id := cacheElectionID(election)
func cacheElectionImage(data []byte, election *api.Election, imageType int) string {
id := generateElectionCacheKey(election, imageType)
imagesLRU.Add(id, data)
return id
}

// electionImageInCache checks if an election associated image exist in the LRU cache.
// electionImageCacheKey checks if an election associated image exist in the LRU cache.
// If so it returns the cache key identifier, otherwise it returns an empty string.
func electionImageInCache(election *api.Election) string {
id := cacheElectionID(election)
func electionImageCacheKey(election *api.Election, imageType int) string {
id := generateElectionCacheKey(election, imageType)
_, ok := imagesLRU.Get(id)
if !ok {
missesCounter.Add(1)
Expand Down Expand Up @@ -68,7 +78,7 @@ func FromCache(id string) []byte {
// Retry for a maximyum time of 8 seconds if the image is not in the cache
startTime := time.Now()
for {
if time.Since(startTime) > 8*time.Second {
if time.Since(startTime) > TimeoutImageGeneration {
return nil
}
data, ok := imagesLRU.Get(id)
Expand Down
20 changes: 14 additions & 6 deletions imageframe/imageframe.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ const (

BackgroundsDir = "images/"
ImageGeneratorURL = "https://img.frame.vote"

TimeoutImageGeneration = 20 * time.Second
)

const (
imageType = iota
imageTypeQuestion
imageTypeResults
)

var backgroundFrames map[string][]byte
Expand Down Expand Up @@ -108,7 +116,7 @@ func QuestionImage(election *api.Election) (string, error) {
return "", fmt.Errorf("election has no questions")
}
// Check if the image is already in the cache
if id := electionImageInCache(election); id != "" {
if id := electionImageCacheKey(election, imageTypeQuestion); id != "" {
return id, nil
}

Expand All @@ -129,9 +137,9 @@ func QuestionImage(election *api.Election) (string, error) {
log.Warnw("failed to create image", "error", err)
return
}
addElectionImageToCache(png, election)
cacheElectionImage(png, election, imageTypeQuestion)
}()
return cacheElectionID(election), nil
return generateElectionCacheKey(election, imageTypeQuestion), nil
}

// ResultsImage creates an image showing the results of a poll.
Expand All @@ -140,7 +148,7 @@ func ResultsImage(election *api.Election) (string, error) {
return "", fmt.Errorf("election has no questions")
}
// Check if the image is already in the cache
if id := electionImageInCache(election); id != "" {
if id := electionImageCacheKey(election, imageTypeResults); id != "" {
return id, nil
}

Expand All @@ -167,9 +175,9 @@ func ResultsImage(election *api.Election) (string, error) {
log.Warnw("failed to create image", "error", err)
return
}
addElectionImageToCache(png, election)
cacheElectionImage(png, election, imageTypeResults)
}()
return cacheElectionID(election), nil
return generateElectionCacheKey(election, imageTypeResults), nil
}

// AfterVoteImage creates a static image to be displayed after a vote has been cast.
Expand Down
18 changes: 13 additions & 5 deletions mongo/results.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,33 @@ package mongo

import (
"context"
"fmt"
"time"

"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
"go.vocdoni.io/dvote/log"
"go.vocdoni.io/dvote/types"
)

// AddFinalResults adds the final results of an election in PNG format.
// It performs and upsert operation, so it will update the results if they already exist.
func (ms *MongoStorage) AddFinalResults(electionID types.HexBytes, finalPNG []byte) error {
results := &Results{
ElectionID: electionID.String(),
FinalPNG: finalPNG,
}

ctx2, cancel2 := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel2()
_, err := ms.results.InsertOne(ctx2, results)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
opts := options.ReplaceOptions{}
opts.Upsert = new(bool)
*opts.Upsert = true
_, err := ms.results.ReplaceOne(ctx, bson.M{"_id": results.ElectionID}, results, &opts)
if err != nil {
return fmt.Errorf("cannot update object: %w", err)
}
log.Debugw("stored PNG results", "electionID", electionID.String())
return err
return nil
}

// FinalResultsPNG returns the final results of an election in PNG format.
Expand Down
22 changes: 15 additions & 7 deletions results.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,22 @@ func (v *vocdoniHandler) results(msg *apirest.APIdata, ctx *httprouter.HTTPConte
if err != nil {
return fmt.Errorf("failed to create image: %w", err)
}
if err := v.db.AddFinalResults(electionIDbytes, imageframe.FromCache(id)); err != nil {
return fmt.Errorf("failed to add final results to database: %w", err)
}
log.Infow("final results image built ondemand", "electionID", electionID)
if v.checkIfElectionFinishedAndHandle(electionIDbytes, ctx) {
return nil
}
go func() {
if err := v.db.AddFinalResults(electionIDbytes, imageframe.FromCache(id)); err != nil {
log.Errorw(err, "failed to add final results to database")
return
}
log.Infow("final results image built ondemand", "electionID", electionID)
}()

response := strings.ReplaceAll(frame(frameFinalResults), "{image}", imageLink(id))
response = strings.ReplaceAll(response, "{processID}", electionID)
response = strings.ReplaceAll(response, "{title}", "Final results")

ctx.SetResponseContentType("text/html; charset=utf-8")
return ctx.Send([]byte(response), http.StatusOK)
}

// if not final results, create the dynamic PNG image with the results
response := strings.ReplaceAll(frame(frameResults), "{image}", resultsPNGfile(election))
response = strings.ReplaceAll(response, "{title}", election.Metadata.Title["default"])
Expand Down

0 comments on commit c57f1f2

Please sign in to comment.