Added support for per-topic view counters.

Added support for shutdown tasks.
View counters are now saved on graceful shutdown.
Dynamic routes are now tracked by the route view counter.
The uploads route should now be tracked by the route view counter.
Added a WYSIWYG Editor to the profiles for Cosora.
This commit is contained in:
Azareal 2017-12-24 22:08:35 +00:00
parent c7df616f5b
commit 964d219407
11 changed files with 84 additions and 39 deletions

View File

@ -47,7 +47,7 @@ func routeSitemapXml(w http.ResponseWriter, r *http.Request) common.RouteError {
writeXMLHeader(w, r) writeXMLHeader(w, r)
w.Write([]byte("<sitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n")) w.Write([]byte("<sitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n"))
sitemapItem("sitemaps/topics.xml") sitemapItem("sitemaps/topics.xml")
sitemapItem("sitemaps/forums.xml") //sitemapItem("sitemaps/forums.xml")
//sitemapItem("sitemaps/users.xml") //sitemapItem("sitemaps/users.xml")
w.Write([]byte("</sitemapindex>")) w.Write([]byte("</sitemapindex>"))

View File

@ -2,7 +2,6 @@ package common
import ( import (
"database/sql" "database/sql"
"log"
"sync" "sync"
"sync/atomic" "sync/atomic"
@ -28,6 +27,7 @@ func NewChunkedViewCounter() (*ChunkedViewCounter, error) {
} }
AddScheduledFifteenMinuteTask(counter.Tick) // This is run once every fifteen minutes to match the frequency of the RouteViewCounter AddScheduledFifteenMinuteTask(counter.Tick) // This is run once every fifteen minutes to match the frequency of the RouteViewCounter
//AddScheduledSecondTask(counter.Tick) //AddScheduledSecondTask(counter.Tick)
AddShutdownTask(counter.Tick)
return counter, acc.FirstError() return counter, acc.FirstError()
} }
@ -82,6 +82,7 @@ func NewDefaultRouteViewCounter() (*DefaultRouteViewCounter, error) {
} }
AddScheduledFifteenMinuteTask(counter.Tick) // There could be a lot of routes, so we don't want to be running this every second AddScheduledFifteenMinuteTask(counter.Tick) // There could be a lot of routes, so we don't want to be running this every second
//AddScheduledSecondTask(counter.Tick) //AddScheduledSecondTask(counter.Tick)
AddShutdownTask(counter.Tick)
return counter, acc.FirstError() return counter, acc.FirstError()
} }
@ -113,7 +114,7 @@ func (counter *DefaultRouteViewCounter) insertChunk(count int, route int) error
func (counter *DefaultRouteViewCounter) Bump(route int) { func (counter *DefaultRouteViewCounter) Bump(route int) {
// TODO: Test this check // TODO: Test this check
log.Print("counter.routeBuckets[route]: ", counter.routeBuckets[route]) debugLog("counter.routeBuckets[", route, "]: ", counter.routeBuckets[route])
if len(counter.routeBuckets) <= route { if len(counter.routeBuckets) <= route {
return return
} }
@ -157,41 +158,53 @@ func NewDefaultTopicViewCounter() (*DefaultTopicViewCounter, error) {
evenTopics: make(map[int]*RWMutexCounterBucket), evenTopics: make(map[int]*RWMutexCounterBucket),
update: acc.Update("topics").Set("views = views + ?").Where("tid = ?").Prepare(), update: acc.Update("topics").Set("views = views + ?").Where("tid = ?").Prepare(),
} }
AddScheduledFifteenMinuteTask(counter.Tick) // There could be a lot of routes, so we don't want to be running this every second AddScheduledFifteenMinuteTask(counter.Tick) // Who knows how many topics we have queued up, we probably don't want this running too frequently
//AddScheduledSecondTask(counter.Tick) //AddScheduledSecondTask(counter.Tick)
AddShutdownTask(counter.Tick)
return counter, acc.FirstError() return counter, acc.FirstError()
} }
func (counter *DefaultTopicViewCounter) Tick() error { func (counter *DefaultTopicViewCounter) Tick() error {
counter.oddLock.RLock() counter.oddLock.RLock()
for topicID, topic := range counter.oddTopics { oddTopics := counter.oddTopics
counter.oddLock.RUnlock()
for topicID, topic := range oddTopics {
var count int var count int
topic.RLock() topic.RLock()
count = topic.counter count = topic.counter
topic.RUnlock() topic.RUnlock()
// TODO: Only delete the bucket when it's zero to avoid hitting popular topics?
counter.oddLock.Lock()
delete(counter.oddTopics, topicID)
counter.oddLock.Unlock()
err := counter.insertChunk(count, topicID) err := counter.insertChunk(count, topicID)
if err != nil { if err != nil {
return err return err
} }
} }
counter.oddLock.RUnlock()
counter.evenLock.RLock() counter.evenLock.RLock()
for topicID, topic := range counter.evenTopics { evenTopics := counter.evenTopics
counter.evenLock.RUnlock()
for topicID, topic := range evenTopics {
var count int var count int
topic.RLock() topic.RLock()
count = topic.counter count = topic.counter
topic.RUnlock() topic.RUnlock()
// TODO: Only delete the bucket when it's zero to avoid hitting popular topics?
counter.evenLock.Lock()
delete(counter.evenTopics, topicID)
counter.evenLock.Unlock()
err := counter.insertChunk(count, topicID) err := counter.insertChunk(count, topicID)
if err != nil { if err != nil {
return err return err
} }
} }
counter.evenLock.RUnlock()
return nil return nil
} }
// TODO: Optimise this further. E.g. Using IN() on every one view topic. Rinse and repeat for two views, three views, four views and five views.
func (counter *DefaultTopicViewCounter) insertChunk(count int, topicID int) error { func (counter *DefaultTopicViewCounter) insertChunk(count int, topicID int) error {
if count == 0 { if count == 0 {
return nil return nil
@ -204,9 +217,9 @@ func (counter *DefaultTopicViewCounter) insertChunk(count int, topicID int) erro
func (counter *DefaultTopicViewCounter) Bump(topicID int) { func (counter *DefaultTopicViewCounter) Bump(topicID int) {
// Is the ID even? // Is the ID even?
if topicID%2 == 0 { if topicID%2 == 0 {
counter.evenLock.Lock() counter.evenLock.RLock()
topic, ok := counter.evenTopics[topicID] topic, ok := counter.evenTopics[topicID]
counter.evenLock.Unlock() counter.evenLock.RUnlock()
if ok { if ok {
topic.Lock() topic.Lock()
topic.counter++ topic.counter++
@ -219,9 +232,9 @@ func (counter *DefaultTopicViewCounter) Bump(topicID int) {
return return
} }
counter.oddLock.Lock() counter.oddLock.RLock()
topic, ok := counter.oddTopics[topicID] topic, ok := counter.oddTopics[topicID]
counter.oddLock.Unlock() counter.oddLock.RUnlock()
if ok { if ok {
topic.Lock() topic.Lock()
topic.counter++ topic.counter++

View File

@ -21,6 +21,7 @@ type TaskStmts struct {
var ScheduledSecondTasks []func() error var ScheduledSecondTasks []func() error
var ScheduledFifteenMinuteTasks []func() error var ScheduledFifteenMinuteTasks []func() error
var ShutdownTasks []func() error
var taskStmts TaskStmts var taskStmts TaskStmts
var lastSync time.Time var lastSync time.Time
@ -45,6 +46,11 @@ func AddScheduledFifteenMinuteTask(task func() error) {
ScheduledFifteenMinuteTasks = append(ScheduledFifteenMinuteTasks, task) ScheduledFifteenMinuteTasks = append(ScheduledFifteenMinuteTasks, task)
} }
// AddShutdownTask is not concurrency safe
func AddShutdownTask(task func() error) {
ShutdownTasks = append(ShutdownTasks, task)
}
// TODO: Use AddScheduledSecondTask // TODO: Use AddScheduledSecondTask
func HandleExpiredScheduledGroups() error { func HandleExpiredScheduledGroups() error {
rows, err := taskStmts.getExpiredScheduledGroups.Query() rows, err := taskStmts.getExpiredScheduledGroups.Query()

View File

@ -72,6 +72,8 @@ var RouteMap = map[string]interface{}{
"routeUnban": routeUnban, "routeUnban": routeUnban,
"routeActivate": routeActivate, "routeActivate": routeActivate,
"routeIps": routeIps, "routeIps": routeIps,
"routeDynamic": routeDynamic,
"routeUploads": routeUploads,
} }
// ! NEVER RELY ON THESE REMAINING THE SAME BETWEEN COMMITS // ! NEVER RELY ON THESE REMAINING THE SAME BETWEEN COMMITS
@ -133,6 +135,8 @@ var routeMapEnum = map[string]int{
"routeUnban": 54, "routeUnban": 54,
"routeActivate": 55, "routeActivate": 55,
"routeIps": 56, "routeIps": 56,
"routeDynamic": 57,
"routeUploads": 58,
} }
var reverseRouteMapEnum = map[int]string{ var reverseRouteMapEnum = map[int]string{
0: "routeAPI", 0: "routeAPI",
@ -192,6 +196,8 @@ var reverseRouteMapEnum = map[int]string{
54: "routeUnban", 54: "routeUnban",
55: "routeActivate", 55: "routeActivate",
56: "routeIps", 56: "routeIps",
57: "routeDynamic",
58: "routeUploads",
} }
// TODO: Stop spilling these into the package scope? // TODO: Stop spilling these into the package scope?
@ -794,6 +800,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
common.NotFound(w,req) common.NotFound(w,req)
return return
} }
common.RouteViewCounter.Bump(58)
req.URL.Path += extraData req.URL.Path += extraData
// TODO: Find a way to propagate errors up from this? // TODO: Find a way to propagate errors up from this?
router.UploadHandler(w,req) // TODO: Count these views router.UploadHandler(w,req) // TODO: Count these views
@ -837,8 +844,9 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
router.RUnlock() router.RUnlock()
if ok { if ok {
common.RouteViewCounter.Bump(57) // TODO: Be more specific about *which* dynamic route it is
req.URL.Path += extraData req.URL.Path += extraData
err = handle(w,req,user) // TODO: Count these views err = handle(w,req,user)
if err != nil { if err != nil {
router.handleError(err,w,req,user) router.handleError(err,w,req,user)
} }

28
main.go
View File

@ -234,6 +234,14 @@ func main() {
} }
} }
var runTasks = func(tasks []func() error) {
for _, task := range tasks {
if task() != nil {
common.LogError(err)
}
}
}
// Run this goroutine once a second // Run this goroutine once a second
secondTicker := time.NewTicker(1 * time.Second) secondTicker := time.NewTicker(1 * time.Second)
fifteenMinuteTicker := time.NewTicker(15 * time.Minute) fifteenMinuteTicker := time.NewTicker(15 * time.Minute)
@ -242,22 +250,16 @@ func main() {
for { for {
select { select {
case <-secondTicker.C: case <-secondTicker.C:
//log.Print("Running the second ticker")
// TODO: Add a plugin hook here // TODO: Add a plugin hook here
runTasks(common.ScheduledSecondTasks)
for _, task := range common.ScheduledSecondTasks { // TODO: Stop hard-coding this
if task() != nil {
common.LogError(err)
}
}
err := common.HandleExpiredScheduledGroups() err := common.HandleExpiredScheduledGroups()
if err != nil { if err != nil {
common.LogError(err) common.LogError(err)
} }
// TODO: Handle delayed moderation tasks // TODO: Handle delayed moderation tasks
// TODO: Handle the daily clean-up. Move this to a 24 hour task?
// Sync with the database, if there are any changes // Sync with the database, if there are any changes
err = common.HandleServerSync() err = common.HandleServerSync()
@ -273,18 +275,15 @@ func main() {
// TODO: Add a plugin hook here // TODO: Add a plugin hook here
case <-fifteenMinuteTicker.C: case <-fifteenMinuteTicker.C:
// TODO: Add a plugin hook here // TODO: Add a plugin hook here
runTasks(common.ScheduledFifteenMinuteTasks)
for _, task := range common.ScheduledFifteenMinuteTasks {
if task() != nil {
common.LogError(err)
}
}
// TODO: Automatically lock topics, if they're really old, and the associated setting is enabled. // TODO: Automatically lock topics, if they're really old, and the associated setting is enabled.
// TODO: Publish scheduled posts. // TODO: Publish scheduled posts.
// TODO: Add a plugin hook here // TODO: Add a plugin hook here
} }
// TODO: Handle the daily clean-up.
} }
}() }()
@ -329,6 +328,7 @@ func main() {
go func() { go func() {
sig := <-sigs sig := <-sigs
// TODO: Gracefully shutdown the HTTP server // TODO: Gracefully shutdown the HTTP server
runTasks(common.ShutdownTasks)
log.Fatal("Received a signal to shutdown: ", sig) log.Fatal("Received a signal to shutdown: ", sig)
}() }()

View File

@ -150,6 +150,9 @@ func main() {
}` }`
} }
// Stubs for us to refer to these routes through
mapIt("routeDynamic")
mapIt("routeUploads")
tmplVars.AllRouteNames = allRouteNames tmplVars.AllRouteNames = allRouteNames
tmplVars.AllRouteMap = allRouteMap tmplVars.AllRouteMap = allRouteMap
@ -294,6 +297,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
common.NotFound(w,req) common.NotFound(w,req)
return return
} }
common.RouteViewCounter.Bump({{.AllRouteMap.routeUploads}})
req.URL.Path += extraData req.URL.Path += extraData
// TODO: Find a way to propagate errors up from this? // TODO: Find a way to propagate errors up from this?
router.UploadHandler(w,req) // TODO: Count these views router.UploadHandler(w,req) // TODO: Count these views
@ -337,8 +341,9 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
router.RUnlock() router.RUnlock()
if ok { if ok {
common.RouteViewCounter.Bump({{.AllRouteMap.routeDynamic}}) // TODO: Be more specific about *which* dynamic route it is
req.URL.Path += extraData req.URL.Path += extraData
err = handle(w,req,user) // TODO: Count these views err = handle(w,req,user)
if err != nil { if err != nil {
router.handleError(err,w,req,user) router.handleError(err,w,req,user)
} }

View File

@ -40,6 +40,12 @@ func (red *HTTPSRedirect) ServeHTTP(w http.ResponseWriter, req *http.Request) {
http.Redirect(w, req, dest, http.StatusTemporaryRedirect) http.Redirect(w, req, dest, http.StatusTemporaryRedirect)
} }
// Temporary stubs for view tracking
func routeDynamic() {
}
func routeUploads() {
}
// GET functions // GET functions
func routeStatic(w http.ResponseWriter, r *http.Request) { func routeStatic(w http.ResponseWriter, r *http.Request) {
file, ok := common.StaticFiles.Get(r.URL.Path) file, ok := common.StaticFiles.Get(r.URL.Path)
@ -613,6 +619,7 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user common.User) comm
if err != nil { if err != nil {
return common.InternalError(err, w, r) return common.InternalError(err, w, r)
} }
common.TopicViewCounter.Bump(topic.ID) // TODO Move this into the router?
return nil return nil
} }

View File

@ -601,8 +601,7 @@ var profile_21 = []byte(`
<div id="profile_comments_head" class="colstack_item colstack_head hash_hide"> <div id="profile_comments_head" class="colstack_item colstack_head hash_hide">
<div class="rowitem"><h1><a>Comments</a></h1></div> <div class="rowitem"><h1><a>Comments</a></h1></div>
</div> </div>
<div id="profile_comments" class="colstack_item hash_hide"> <div id="profile_comments" class="colstack_item hash_hide">`)
`)
var profile_comments_row_0 = []byte(` var profile_comments_row_0 = []byte(`
<div class="rowitem passive deletable_block editable_parent simple `) <div class="rowitem passive deletable_block editable_parent simple `)
var profile_comments_row_1 = []byte(`" style="background-image: url(`) var profile_comments_row_1 = []byte(`" style="background-image: url(`)
@ -672,8 +671,7 @@ var profile_comments_row_33 = []byte(`&type=user-reply"><button class="username
</div> </div>
</div> </div>
`) `)
var profile_22 = []byte(` var profile_22 = []byte(`</div>
</div>
`) `)
var profile_23 = []byte(` var profile_23 = []byte(`
@ -682,9 +680,9 @@ var profile_23 = []byte(`
var profile_24 = []byte(`' type="hidden" /> var profile_24 = []byte(`' type="hidden" />
<div class="colstack_item topic_reply_form" style="border-top: none;"> <div class="colstack_item topic_reply_form" style="border-top: none;">
<div class="formrow"> <div class="formrow">
<div class="formitem"><textarea name="reply-content" placeholder="Insert reply here"></textarea></div> <div class="formitem"><textarea class="input_content" name="reply-content" placeholder="Insert comment here"></textarea></div>
</div> </div>
<div class="formrow"> <div class="formrow quick_button_row">
<div class="formitem"><button name="reply-button" class="formbutton">Create Reply</button></div> <div class="formitem"><button name="reply-button" class="formbutton">Create Reply</button></div>
</div> </div>
</div> </div>

View File

@ -69,18 +69,16 @@
<div id="profile_comments_head" class="colstack_item colstack_head hash_hide"> <div id="profile_comments_head" class="colstack_item colstack_head hash_hide">
<div class="rowitem"><h1><a>Comments</a></h1></div> <div class="rowitem"><h1><a>Comments</a></h1></div>
</div> </div>
<div id="profile_comments" class="colstack_item hash_hide"> <div id="profile_comments" class="colstack_item hash_hide">{{template "profile_comments_row.html" . }}</div>
{{template "profile_comments_row.html" . }}
</div>
{{if not .CurrentUser.IsBanned}} {{if not .CurrentUser.IsBanned}}
<form id="profile_comments_form" class="hash_hide" action="/profile/reply/create/" method="post"> <form id="profile_comments_form" class="hash_hide" action="/profile/reply/create/" method="post">
<input name="uid" value='{{.ProfileOwner.ID}}' type="hidden" /> <input name="uid" value='{{.ProfileOwner.ID}}' type="hidden" />
<div class="colstack_item topic_reply_form" style="border-top: none;"> <div class="colstack_item topic_reply_form" style="border-top: none;">
<div class="formrow"> <div class="formrow">
<div class="formitem"><textarea name="reply-content" placeholder="Insert reply here"></textarea></div> <div class="formitem"><textarea class="input_content" name="reply-content" placeholder="Insert comment here"></textarea></div>
</div> </div>
<div class="formrow"> <div class="formrow quick_button_row">
<div class="formitem"><button name="reply-button" class="formbutton">Create Reply</button></div> <div class="formitem"><button name="reply-button" class="formbutton">Create Reply</button></div>
</div> </div>
</div> </div>

View File

@ -479,6 +479,9 @@ select, input, textarea, button {
width: 100%; width: 100%;
height: min-content; height: min-content;
} }
.topic_reply_form .formrow {
padding: 0px !important;
}
.topic_reply_form .trumbowyg-button-pane:after { .topic_reply_form .trumbowyg-button-pane:after {
display: none; display: none;
} }
@ -935,6 +938,9 @@ select, input, textarea, button {
#profile_comments { #profile_comments {
margin-bottom: 12px; margin-bottom: 12px;
} }
#profile_comments:empty {
display: none !important;
}
#profile_comments .rowitem { #profile_comments .rowitem {
background-image: none !important; background-image: none !important;
} }

View File

@ -25,6 +25,10 @@ $(document).ready(function(){
btns: btnlist, btns: btnlist,
autogrow: true, autogrow: true,
}); });
$('#profile_comments_form .topic_reply_form .input_content').trumbowyg({
btns: [['viewHTML'],['strong','em','del'],['link'],['insertImage'],['removeformat']],
autogrow: true,
});
// TODO: Refactor this to use `each` less // TODO: Refactor this to use `each` less
$('.button_menu').click(function(){ $('.button_menu').click(function(){