Skip to content

Commit

Permalink
feat(api): add server-side caching for requests that could benefit (#…
Browse files Browse the repository at this point in the history
…3463)

* feat(api): add server-side caching for requests that could benefit for them

* fix(tests): do not cache responses while in tests

* fix: remove commented out leftover code

* chore(deps): update dependency html-webpack-plugin to v5.5.4

* Bundle embedded web app

* fix: remove caching for web app assets under test

* chore(tests): re-enable temporarily disabled test

* chore(deps): update dependency typescript to v5.3.3

* Bundle embedded web app

* chore(deps): update dependency npm to v10.2.5

* Bundle embedded web app

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Owncast <owncast@owncast.online>
  • Loading branch information
3 people committed Dec 10, 2023
1 parent b6efe49 commit 2217f06
Show file tree
Hide file tree
Showing 21 changed files with 317 additions and 120 deletions.
4 changes: 4 additions & 0 deletions config/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,8 @@ var (

// PublicFilesPath is the optional directory for hosting public files.
PublicFilesPath = filepath.Join(DataDirectory, "public")

// DisableResponseCaching will disable caching of API and resource
// responses. Disable this feature to turn off the optimizations.
DisableResponseCaching = false
)
36 changes: 36 additions & 0 deletions controllers/hls.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,27 @@ import (
"path/filepath"
"strconv"
"strings"
"time"

"github.com/owncast/owncast/config"
"github.com/owncast/owncast/core"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/router/middleware"
"github.com/owncast/owncast/utils"
log "github.com/sirupsen/logrus"
cache "github.com/victorspringer/http-cache"
"github.com/victorspringer/http-cache/adapter/memory"
)

type FileServerHandler struct {
HLSPath string
}

func (fsh *FileServerHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
http.ServeFile(rw, r, fsh.HLSPath)
}

// HandleHLSRequest will manage all requests to HLS content.
func HandleHLSRequest(w http.ResponseWriter, r *http.Request) {
// Sanity check to limit requests to HLS file types.
Expand All @@ -23,6 +35,26 @@ func HandleHLSRequest(w http.ResponseWriter, r *http.Request) {
return
}

responseCache, err := memory.NewAdapter(
memory.AdapterWithAlgorithm(memory.LRU),
memory.AdapterWithCapacity(20),
memory.AdapterWithStorageCapacity(209_715_200),
)
if err != nil {
log.Warn("unable to create web cache", err)
}

// Since HLS segments cannot be changed once they're rendered, we can cache
// individual segments for a long time.
longTermHLSSegmentCache, err := cache.NewClient(
cache.ClientWithAdapter(responseCache),
cache.ClientWithTTL(30*time.Second),
cache.ClientWithExpiresHeader(),
)
if err != nil {
log.Warn("unable to create web cache client", err)
}

requestedPath := r.URL.Path
relativePath := strings.Replace(requestedPath, "/hls/", "", 1)
fullPath := filepath.Join(config.HLSStoragePath, relativePath)
Expand All @@ -48,6 +80,10 @@ func HandleHLSRequest(w http.ResponseWriter, r *http.Request) {
} else {
cacheTime := utils.GetCacheDurationSecondsForPath(relativePath)
w.Header().Set("Cache-Control", "public, max-age="+strconv.Itoa(cacheTime))

fileServer := &FileServerHandler{HLSPath: fullPath}
longTermHLSSegmentCache.Middleware(fileServer).ServeHTTP(w, r)
return
}

middleware.EnableCors(w)
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ require (
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/oschwald/maxminddb-golang v1.11.0 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/victorspringer/http-cache v0.0.0-20231006141456-6446fe59efba // indirect
)

require (
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9f
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/valyala/gozstd v1.20.1 h1:xPnnnvjmaDDitMFfDxmQ4vpx0+3CdTg2o3lALvXTU/g=
github.com/valyala/gozstd v1.20.1/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ=
github.com/victorspringer/http-cache v0.0.0-20231006141456-6446fe59efba h1:+oqDKQIOdkkvro1psUKtI4oH9WBeKkGY2S8h9/lo288=
github.com/victorspringer/http-cache v0.0.0-20231006141456-6446fe59efba/go.mod h1:D1AD6nlXv7HkIfTVd8ZWK1KQEiXYNy/LbLkx8H9tIQw=
github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=
Expand Down
27 changes: 16 additions & 11 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,18 @@ import (
)

var (
dbFile = flag.String("database", "", "Path to the database file.")
logDirectory = flag.String("logdir", "", "Directory where logs will be written to")
backupDirectory = flag.String("backupdir", "", "Directory where backups will be written to")
enableDebugOptions = flag.Bool("enableDebugFeatures", false, "Enable additional debugging options.")
enableVerboseLogging = flag.Bool("enableVerboseLogging", false, "Enable additional logging.")
restoreDatabaseFile = flag.String("restoreDatabase", "", "Restore an Owncast database backup")
newAdminPassword = flag.String("adminpassword", "", "Set your admin password")
newStreamKey = flag.String("streamkey", "", "Set a temporary stream key for this session")
webServerPortOverride = flag.String("webserverport", "", "Force the web server to listen on a specific port")
webServerIPOverride = flag.String("webserverip", "", "Force web server to listen on this IP address")
rtmpPortOverride = flag.Int("rtmpport", 0, "Set listen port for the RTMP server")
dbFile = flag.String("database", "", "Path to the database file.")
logDirectory = flag.String("logdir", "", "Directory where logs will be written to")
backupDirectory = flag.String("backupdir", "", "Directory where backups will be written to")
enableDebugOptions = flag.Bool("enableDebugFeatures", false, "Enable additional debugging options.")
enableVerboseLogging = flag.Bool("enableVerboseLogging", false, "Enable additional logging.")
restoreDatabaseFile = flag.String("restoreDatabase", "", "Restore an Owncast database backup")
newAdminPassword = flag.String("adminpassword", "", "Set your admin password")
newStreamKey = flag.String("streamkey", "", "Set a temporary stream key for this session")
webServerPortOverride = flag.String("webserverport", "", "Force the web server to listen on a specific port")
webServerIPOverride = flag.String("webserverip", "", "Force web server to listen on this IP address")
rtmpPortOverride = flag.Int("rtmpport", 0, "Set listen port for the RTMP server")
disableResponseCaching = flag.Bool("disableResponseCaching", false, "Do not optimize performance by caching of web responses")
)

// nolint:cyclop
Expand All @@ -42,6 +43,10 @@ func main() {
config.BackupDirectory = *backupDirectory
}

if *disableResponseCaching {
config.DisableResponseCaching = *disableResponseCaching
}

// Create the data directory if needed
if !utils.DoesFileExists("data") {
if err := os.Mkdir("./data", 0o700); err != nil {
Expand Down
109 changes: 95 additions & 14 deletions router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,52 +24,131 @@ import (
"github.com/owncast/owncast/router/middleware"
"github.com/owncast/owncast/utils"
"github.com/owncast/owncast/yp"

cache "github.com/victorspringer/http-cache"
"github.com/victorspringer/http-cache/adapter/memory"
)

// Start starts the router for the http, ws, and rtmp.
func Start() error {
// Setup a web response cache
enableCache := !config.DisableResponseCaching

responseCache, err := memory.NewAdapter(
memory.AdapterWithAlgorithm(memory.LRU),
memory.AdapterWithCapacity(50),
)
if err != nil {
log.Warn("unable to create web cache", err)
}

superShortCacheClient, err := cache.NewClient(
cache.ClientWithAdapter(responseCache),
cache.ClientWithTTL(3*time.Second),
)
if err != nil {
log.Warn("unable to create web cache client", err)
}
reasonableDurationCacheClient, err := cache.NewClient(
cache.ClientWithAdapter(responseCache),
cache.ClientWithTTL(8*time.Second),
)
if err != nil {
log.Warn("unable to create web cache client", err)
}
longerDurationCacheClient, err := cache.NewClient(
cache.ClientWithAdapter(responseCache),
cache.ClientWithTTL(3*time.Minute),
)
if err != nil {
log.Warn("unable to create web cache client", err)
}
// The primary web app.
http.HandleFunc("/", controllers.IndexHandler)
if enableCache {
http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
longerDurationCacheClient.Middleware(http.HandlerFunc(controllers.IndexHandler)).ServeHTTP(rw, r)
})
} else {
http.HandleFunc("/", controllers.IndexHandler)
}

// The admin web app.
http.HandleFunc("/admin/", middleware.RequireAdminAuth(controllers.IndexHandler))

// Images
http.HandleFunc("/thumbnail.jpg", controllers.GetThumbnail)
http.HandleFunc("/preview.gif", controllers.GetPreview)
http.HandleFunc("/logo", controllers.GetLogo)
http.HandleFunc("/thumbnail.jpg", func(rw http.ResponseWriter, r *http.Request) {
superShortCacheClient.Middleware(http.HandlerFunc(controllers.GetThumbnail)).ServeHTTP(rw, r)
})

http.HandleFunc("/preview.gif", func(rw http.ResponseWriter, r *http.Request) {
superShortCacheClient.Middleware(http.HandlerFunc(controllers.GetPreview)).ServeHTTP(rw, r)
})

http.HandleFunc("/logo", func(rw http.ResponseWriter, r *http.Request) {
longerDurationCacheClient.Middleware(http.HandlerFunc(controllers.GetLogo)).ServeHTTP(rw, r)
})

// Custom Javascript
http.HandleFunc("/customjavascript", controllers.ServeCustomJavascript)
http.HandleFunc("/customjavascript", func(rw http.ResponseWriter, r *http.Request) {
longerDurationCacheClient.Middleware(http.HandlerFunc(controllers.ServeCustomJavascript)).ServeHTTP(rw, r)
})

// Return a single emoji image.
http.HandleFunc(config.EmojiDir, controllers.GetCustomEmojiImage)
http.HandleFunc(config.EmojiDir, func(rw http.ResponseWriter, r *http.Request) {
longerDurationCacheClient.Middleware(http.HandlerFunc(controllers.GetCustomEmojiImage)).ServeHTTP(rw, r)
})

// return the logo

// return a logo that's compatible with external social networks
http.HandleFunc("/logo/external", controllers.GetCompatibleLogo)
http.HandleFunc("/logo/external", func(rw http.ResponseWriter, r *http.Request) {
longerDurationCacheClient.Middleware(http.HandlerFunc(controllers.GetCompatibleLogo)).ServeHTTP(rw, r)
})

// robots.txt
http.HandleFunc("/robots.txt", controllers.GetRobotsDotTxt)
http.HandleFunc("/robots.txt", func(rw http.ResponseWriter, r *http.Request) {
longerDurationCacheClient.Middleware(http.HandlerFunc(controllers.GetRobotsDotTxt)).ServeHTTP(rw, r)
})

// status of the system
http.HandleFunc("/api/status", controllers.GetStatus)
if enableCache {
http.HandleFunc("/api/status", func(rw http.ResponseWriter, r *http.Request) {
superShortCacheClient.Middleware(http.HandlerFunc(controllers.GetStatus)).ServeHTTP(rw, r)
})
} else {
http.HandleFunc("/api/status", controllers.GetStatus)
}

// custom emoji supported in the chat
http.HandleFunc("/api/emoji", controllers.GetCustomEmojiList)
http.HandleFunc("/api/emoji", func(rw http.ResponseWriter, r *http.Request) {
reasonableDurationCacheClient.Middleware(http.HandlerFunc(controllers.GetCustomEmojiList)).ServeHTTP(rw, r)
})

// chat rest api
http.HandleFunc("/api/chat", middleware.RequireUserAccessToken(controllers.GetChatMessages))
if enableCache {
http.HandleFunc("/api/chat", func(rw http.ResponseWriter, r *http.Request) {
superShortCacheClient.Middleware(middleware.RequireUserAccessToken(controllers.GetChatMessages))
})
} else {
http.HandleFunc("/api/chat", middleware.RequireUserAccessToken(controllers.GetChatMessages))
}

// web config api
http.HandleFunc("/api/config", controllers.GetWebConfig)
if enableCache {
http.HandleFunc("/api/config", func(rw http.ResponseWriter, r *http.Request) {
superShortCacheClient.Middleware(http.HandlerFunc(controllers.GetWebConfig)).ServeHTTP(rw, r)
})
} else {
http.HandleFunc("/api/config", controllers.GetWebConfig)
}

// return the YP protocol data
http.HandleFunc("/api/yp", yp.GetYPResponse)

// list of all social platforms
http.HandleFunc("/api/socialplatforms", controllers.GetAllSocialPlatforms)
http.HandleFunc("/api/socialplatforms", func(rw http.ResponseWriter, r *http.Request) {
reasonableDurationCacheClient.Middleware(http.HandlerFunc(controllers.GetAllSocialPlatforms)).ServeHTTP(rw, r)
})

// return the list of video variants available
http.HandleFunc("/api/video/variants", controllers.GetVideoStreamOutputVariants)
Expand All @@ -84,7 +163,9 @@ func Start() error {
http.HandleFunc("/api/remotefollow", controllers.RemoteFollow)

// return followers
http.HandleFunc("/api/followers", middleware.HandlePagination(controllers.GetFollowers))
http.HandleFunc("/api/followers", func(rw http.ResponseWriter, r *http.Request) {
reasonableDurationCacheClient.Middleware(middleware.HandlePagination(controllers.GetFollowers)).ServeHTTP(rw, r)
})

// save client video playback metrics
http.HandleFunc("/api/metrics/playback", controllers.ReportPlaybackMetrics)
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
self.__SSG_MANIFEST=new Set,self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB();
1 change: 1 addition & 0 deletions static/web/_next/static/chunks/4281-f024035bb909af4e.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions static/web/_next/static/chunks/5056-b14d7a3d2aee94c3.js

Large diffs are not rendered by default.

0 comments on commit 2217f06

Please sign in to comment.