2017-09-13 15:09:13 +00:00
/ *
*
* Gosora Topic Store
* Copyright Azareal 2017 - 2018
*
* /
2017-06-13 07:12:58 +00:00
package main
2017-09-15 22:20:01 +00:00
import (
"database/sql"
"log"
"sync"
2017-09-18 17:03:52 +00:00
"sync/atomic"
2017-09-15 22:20:01 +00:00
"./query_gen/lib"
)
2017-06-13 07:12:58 +00:00
2017-09-10 16:57:22 +00:00
// TODO: Add the watchdog goroutine
2017-09-15 22:20:01 +00:00
// TODO: Add BulkGetMap
// TODO: Add some sort of update method
2017-09-18 17:03:52 +00:00
// ? - Should we add stick, lock, unstick, and unlock methods? These might be better on the Topics not the TopicStore
2017-06-13 07:12:58 +00:00
var topics TopicStore
type TopicStore interface {
2017-09-15 22:20:01 +00:00
Reload ( id int ) error // ? - Should we move this to TopicCache? Might require us to do a lot more casting in Gosora though...
2017-06-13 07:12:58 +00:00
Get ( id int ) ( * Topic , error )
BypassGet ( id int ) ( * Topic , error )
2017-09-15 22:20:01 +00:00
Delete ( id int ) error
Exists ( id int ) bool
2017-09-18 17:03:52 +00:00
AddLastTopic ( item * Topic , fid int ) error // unimplemented
// TODO: Implement these two methods
//GetReplies() ([]*Reply, error)
//GetRepliesRange(lower int, higher int) ([]*Reply, error)
2017-09-15 22:20:01 +00:00
GetGlobalCount ( ) int
}
type TopicCache interface {
CacheGet ( id int ) ( * Topic , error )
2017-09-18 17:03:52 +00:00
CacheGetUnsafe ( id int ) ( * Topic , error )
2017-09-15 22:20:01 +00:00
CacheSet ( item * Topic ) error
CacheAdd ( item * Topic ) error
CacheAddUnsafe ( item * Topic ) error
CacheRemove ( id int ) error
CacheRemoveUnsafe ( id int ) error
2017-09-18 17:03:52 +00:00
Flush ( )
2017-06-13 07:12:58 +00:00
GetLength ( ) int
2017-09-15 22:20:01 +00:00
SetCapacity ( capacity int )
2017-06-13 07:12:58 +00:00
GetCapacity ( ) int
}
2017-06-15 11:40:35 +00:00
type MemoryTopicStore struct {
2017-09-15 22:20:01 +00:00
items map [ int ] * Topic
2017-09-18 17:03:52 +00:00
length int64 // sync/atomic only lets us operate on int32s and int64s
2017-09-15 22:20:01 +00:00
capacity int
get * sql . Stmt
exists * sql . Stmt
topicCount * sql . Stmt
2017-09-18 17:03:52 +00:00
delete * sql . Stmt
2017-06-13 07:12:58 +00:00
sync . RWMutex
}
2017-09-13 15:09:13 +00:00
// NewMemoryTopicStore gives you a new instance of MemoryTopicStore
2017-06-15 11:40:35 +00:00
func NewMemoryTopicStore ( capacity int ) * MemoryTopicStore {
2017-09-15 22:20:01 +00:00
getStmt , err := qgen . Builder . SimpleSelect ( "topics" , "title, content, createdBy, createdAt, is_closed, sticky, parentID, ipaddress, postCount, likeCount, data" , "tid = ?" , "" , "" )
if err != nil {
log . Fatal ( err )
}
existsStmt , err := qgen . Builder . SimpleSelect ( "topics" , "tid" , "tid = ?" , "" , "" )
if err != nil {
log . Fatal ( err )
}
topicCountStmt , err := qgen . Builder . SimpleCount ( "topics" , "" , "" )
2017-06-13 08:56:48 +00:00
if err != nil {
log . Fatal ( err )
}
2017-09-18 17:03:52 +00:00
deleteStmt , err := qgen . Builder . SimpleDelete ( "topics" , "tid = ?" )
if err != nil {
log . Fatal ( err )
}
2017-06-15 11:40:35 +00:00
return & MemoryTopicStore {
2017-09-15 22:20:01 +00:00
items : make ( map [ int ] * Topic ) ,
capacity : capacity ,
get : getStmt ,
exists : existsStmt ,
topicCount : topicCountStmt ,
2017-09-18 17:03:52 +00:00
delete : deleteStmt ,
2017-06-13 08:56:48 +00:00
}
2017-06-13 07:12:58 +00:00
}
2017-09-18 17:03:52 +00:00
func ( mts * MemoryTopicStore ) CacheGet ( id int ) ( * Topic , error ) {
mts . RLock ( )
item , ok := mts . items [ id ]
mts . RUnlock ( )
2017-06-13 07:12:58 +00:00
if ok {
return item , nil
}
2017-06-28 12:05:26 +00:00
return item , ErrNoRows
2017-06-13 07:12:58 +00:00
}
2017-09-18 17:03:52 +00:00
func ( mts * MemoryTopicStore ) CacheGetUnsafe ( id int ) ( * Topic , error ) {
item , ok := mts . items [ id ]
2017-06-13 07:12:58 +00:00
if ok {
return item , nil
}
2017-06-28 12:05:26 +00:00
return item , ErrNoRows
2017-06-13 07:12:58 +00:00
}
2017-09-18 17:03:52 +00:00
func ( mts * MemoryTopicStore ) Get ( id int ) ( * Topic , error ) {
mts . RLock ( )
topic , ok := mts . items [ id ]
mts . RUnlock ( )
2017-06-13 07:12:58 +00:00
if ok {
return topic , nil
}
2017-09-03 04:50:31 +00:00
topic = & Topic { ID : id }
2017-09-18 17:03:52 +00:00
err := mts . get . QueryRow ( id ) . Scan ( & topic . Title , & topic . Content , & topic . CreatedBy , & topic . CreatedAt , & topic . IsClosed , & topic . Sticky , & topic . ParentID , & topic . IPAddress , & topic . PostCount , & topic . LikeCount , & topic . Data )
2017-06-13 07:12:58 +00:00
if err == nil {
2017-09-03 04:50:31 +00:00
topic . Link = buildTopicURL ( nameToSlug ( topic . Title ) , id )
2017-09-18 17:03:52 +00:00
_ = mts . CacheAdd ( topic )
2017-06-13 07:12:58 +00:00
}
return topic , err
}
2017-09-18 17:03:52 +00:00
// BypassGet will always bypass the cache and pull the topic directly from the database
func ( mts * MemoryTopicStore ) BypassGet ( id int ) ( * Topic , error ) {
2017-09-03 04:50:31 +00:00
topic := & Topic { ID : id }
2017-09-18 17:03:52 +00:00
err := mts . get . QueryRow ( id ) . Scan ( & topic . Title , & topic . Content , & topic . CreatedBy , & topic . CreatedAt , & topic . IsClosed , & topic . Sticky , & topic . ParentID , & topic . IPAddress , & topic . PostCount , & topic . LikeCount , & topic . Data )
2017-09-03 04:50:31 +00:00
topic . Link = buildTopicURL ( nameToSlug ( topic . Title ) , id )
2017-06-13 07:12:58 +00:00
return topic , err
}
2017-09-18 17:03:52 +00:00
func ( mts * MemoryTopicStore ) Reload ( id int ) error {
2017-09-03 04:50:31 +00:00
topic := & Topic { ID : id }
2017-09-18 17:03:52 +00:00
err := mts . get . QueryRow ( id ) . Scan ( & topic . Title , & topic . Content , & topic . CreatedBy , & topic . CreatedAt , & topic . IsClosed , & topic . Sticky , & topic . ParentID , & topic . IPAddress , & topic . PostCount , & topic . LikeCount , & topic . Data )
2017-06-13 07:12:58 +00:00
if err == nil {
2017-09-03 04:50:31 +00:00
topic . Link = buildTopicURL ( nameToSlug ( topic . Title ) , id )
2017-09-18 17:03:52 +00:00
_ = mts . CacheSet ( topic )
2017-06-13 07:12:58 +00:00
} else {
2017-09-18 17:03:52 +00:00
_ = mts . CacheRemove ( id )
2017-06-13 07:12:58 +00:00
}
return err
}
2017-09-15 22:20:01 +00:00
// TODO: Use a transaction here
2017-09-18 17:03:52 +00:00
func ( mts * MemoryTopicStore ) Delete ( id int ) error {
topic , err := mts . Get ( id )
2017-09-15 22:20:01 +00:00
if err != nil {
return nil // Already gone, maybe we should check for other errors here
}
topicCreator , err := users . Get ( topic . CreatedBy )
if err == nil {
wcount := wordCount ( topic . Content )
err = topicCreator . decreasePostStats ( wcount , true )
if err != nil {
return err
}
} else if err != ErrNoRows {
return err
}
err = fstore . DecrementTopicCount ( topic . ParentID )
if err != nil && err != ErrNoRows {
return err
}
2017-09-18 17:03:52 +00:00
mts . Lock ( )
mts . CacheRemoveUnsafe ( id )
_ , err = mts . delete . Exec ( id )
mts . Unlock ( )
return err
2017-09-15 22:20:01 +00:00
}
2017-09-18 17:03:52 +00:00
func ( mts * MemoryTopicStore ) Exists ( id int ) bool {
return mts . exists . QueryRow ( id ) . Scan ( & id ) == nil
2017-09-15 22:20:01 +00:00
}
2017-09-18 17:03:52 +00:00
func ( mts * MemoryTopicStore ) CacheSet ( item * Topic ) error {
mts . Lock ( )
_ , ok := mts . items [ item . ID ]
2017-06-13 07:12:58 +00:00
if ok {
2017-09-18 17:03:52 +00:00
mts . items [ item . ID ] = item
} else if int ( mts . length ) >= mts . capacity {
mts . Unlock ( )
2017-06-13 07:12:58 +00:00
return ErrStoreCapacityOverflow
} else {
2017-09-18 17:03:52 +00:00
mts . items [ item . ID ] = item
atomic . AddInt64 ( & mts . length , 1 )
2017-06-13 07:12:58 +00:00
}
2017-09-18 17:03:52 +00:00
mts . Unlock ( )
2017-06-13 07:12:58 +00:00
return nil
}
2017-09-18 17:03:52 +00:00
func ( mts * MemoryTopicStore ) CacheAdd ( item * Topic ) error {
if int ( mts . length ) >= mts . capacity {
2017-06-13 07:12:58 +00:00
return ErrStoreCapacityOverflow
}
2017-09-18 17:03:52 +00:00
mts . Lock ( )
mts . items [ item . ID ] = item
mts . Unlock ( )
atomic . AddInt64 ( & mts . length , 1 )
2017-06-13 07:12:58 +00:00
return nil
}
2017-09-15 22:20:01 +00:00
// TODO: Make these length increments thread-safe. Ditto for the other DataStores
2017-09-18 17:03:52 +00:00
func ( mts * MemoryTopicStore ) CacheAddUnsafe ( item * Topic ) error {
if int ( mts . length ) >= mts . capacity {
2017-06-13 07:12:58 +00:00
return ErrStoreCapacityOverflow
}
2017-09-18 17:03:52 +00:00
mts . items [ item . ID ] = item
atomic . AddInt64 ( & mts . length , 1 )
2017-06-13 07:12:58 +00:00
return nil
}
2017-09-15 22:20:01 +00:00
// TODO: Make these length decrements thread-safe. Ditto for the other DataStores
2017-09-18 17:03:52 +00:00
func ( mts * MemoryTopicStore ) CacheRemove ( id int ) error {
mts . Lock ( )
delete ( mts . items , id )
mts . Unlock ( )
atomic . AddInt64 ( & mts . length , - 1 )
2017-06-13 07:12:58 +00:00
return nil
}
2017-09-18 17:03:52 +00:00
func ( mts * MemoryTopicStore ) CacheRemoveUnsafe ( id int ) error {
delete ( mts . items , id )
atomic . AddInt64 ( & mts . length , - 1 )
2017-06-13 07:12:58 +00:00
return nil
}
2017-09-15 22:20:01 +00:00
// ? - What is this? Do we need it? Should it be in the main store interface?
2017-09-18 17:03:52 +00:00
func ( mts * MemoryTopicStore ) AddLastTopic ( item * Topic , fid int ) error {
2017-06-13 07:12:58 +00:00
// Coming Soon...
return nil
}
2017-09-18 17:03:52 +00:00
func ( mts * MemoryTopicStore ) Flush ( ) {
mts . Lock ( )
mts . items = make ( map [ int ] * Topic )
mts . length = 0
mts . Unlock ( )
}
func ( mts * MemoryTopicStore ) GetLength ( ) int {
return int ( mts . length )
2017-06-13 07:12:58 +00:00
}
2017-09-18 17:03:52 +00:00
func ( mts * MemoryTopicStore ) SetCapacity ( capacity int ) {
mts . capacity = capacity
2017-06-13 07:12:58 +00:00
}
2017-09-18 17:03:52 +00:00
func ( mts * MemoryTopicStore ) GetCapacity ( ) int {
return mts . capacity
2017-06-13 07:12:58 +00:00
}
2017-09-15 22:20:01 +00:00
// Return the total number of topics on these forums
2017-09-18 17:03:52 +00:00
func ( mts * MemoryTopicStore ) GetGlobalCount ( ) int {
2017-09-15 22:20:01 +00:00
var tcount int
2017-09-18 17:03:52 +00:00
err := mts . topicCount . QueryRow ( ) . Scan ( & tcount )
2017-09-15 22:20:01 +00:00
if err != nil {
LogError ( err )
}
return tcount
}
2017-09-10 16:57:22 +00:00
type SQLTopicStore struct {
2017-09-15 22:20:01 +00:00
get * sql . Stmt
exists * sql . Stmt
topicCount * sql . Stmt
2017-09-18 17:03:52 +00:00
delete * sql . Stmt
2017-06-13 07:12:58 +00:00
}
2017-09-10 16:57:22 +00:00
func NewSQLTopicStore ( ) * SQLTopicStore {
2017-09-15 22:20:01 +00:00
getStmt , err := qgen . Builder . SimpleSelect ( "topics" , "title, content, createdBy, createdAt, is_closed, sticky, parentID, ipaddress, postCount, likeCount, data" , "tid = ?" , "" , "" )
2017-06-13 08:56:48 +00:00
if err != nil {
log . Fatal ( err )
}
2017-09-15 22:20:01 +00:00
existsStmt , err := qgen . Builder . SimpleSelect ( "topics" , "tid" , "tid = ?" , "" , "" )
if err != nil {
log . Fatal ( err )
}
topicCountStmt , err := qgen . Builder . SimpleCount ( "topics" , "" , "" )
if err != nil {
log . Fatal ( err )
}
2017-09-18 17:03:52 +00:00
deleteStmt , err := qgen . Builder . SimpleDelete ( "topics" , "tid = ?" )
if err != nil {
log . Fatal ( err )
}
2017-09-15 22:20:01 +00:00
return & SQLTopicStore {
get : getStmt ,
exists : existsStmt ,
topicCount : topicCountStmt ,
2017-09-18 17:03:52 +00:00
delete : deleteStmt ,
2017-09-15 22:20:01 +00:00
}
2017-06-13 07:12:58 +00:00
}
2017-09-10 16:57:22 +00:00
func ( sts * SQLTopicStore ) Get ( id int ) ( * Topic , error ) {
2017-09-03 04:50:31 +00:00
topic := Topic { ID : id }
err := sts . get . QueryRow ( id ) . Scan ( & topic . Title , & topic . Content , & topic . CreatedBy , & topic . CreatedAt , & topic . IsClosed , & topic . Sticky , & topic . ParentID , & topic . IPAddress , & topic . PostCount , & topic . LikeCount , & topic . Data )
topic . Link = buildTopicURL ( nameToSlug ( topic . Title ) , id )
2017-06-13 07:12:58 +00:00
return & topic , err
}
2017-09-18 17:03:52 +00:00
// BypassGet is an alias of Get(), as we don't have a cache for SQLTopicStore
2017-09-10 16:57:22 +00:00
func ( sts * SQLTopicStore ) BypassGet ( id int ) ( * Topic , error ) {
2017-09-03 04:50:31 +00:00
topic := & Topic { ID : id }
err := sts . get . QueryRow ( id ) . Scan ( & topic . Title , & topic . Content , & topic . CreatedBy , & topic . CreatedAt , & topic . IsClosed , & topic . Sticky , & topic . ParentID , & topic . IPAddress , & topic . PostCount , & topic . LikeCount , & topic . Data )
topic . Link = buildTopicURL ( nameToSlug ( topic . Title ) , id )
2017-06-13 07:12:58 +00:00
return topic , err
}
2017-09-18 17:03:52 +00:00
// Reload uses a similar query to Exists(), as we don't have any entries to reload, and the secondary benefit of calling Reload() is seeing if the item you're trying to reload exists
2017-09-15 22:20:01 +00:00
func ( sts * SQLTopicStore ) Reload ( id int ) error {
return sts . exists . QueryRow ( id ) . Scan ( & id )
2017-06-13 07:12:58 +00:00
}
2017-09-15 22:20:01 +00:00
func ( sts * SQLTopicStore ) Exists ( id int ) bool {
return sts . exists . QueryRow ( id ) . Scan ( & id ) == nil
2017-06-13 07:12:58 +00:00
}
2017-09-15 22:20:01 +00:00
// TODO: Use a transaction here
func ( sts * SQLTopicStore ) Delete ( id int ) error {
topic , err := sts . Get ( id )
if err != nil {
return nil // Already gone, maybe we should check for other errors here
}
topicCreator , err := users . Get ( topic . CreatedBy )
if err == nil {
wcount := wordCount ( topic . Content )
err = topicCreator . decreasePostStats ( wcount , true )
if err != nil {
return err
}
} else if err != ErrNoRows {
return err
}
err = fstore . DecrementTopicCount ( topic . ParentID )
if err != nil && err != ErrNoRows {
return err
}
2017-09-18 17:03:52 +00:00
_ , err = sts . delete . Exec ( id )
2017-09-15 22:20:01 +00:00
return err
2017-06-13 07:12:58 +00:00
}
2017-09-15 22:20:01 +00:00
2017-09-18 17:03:52 +00:00
// ? - What're we going to do about this?
2017-09-10 16:57:22 +00:00
func ( sts * SQLTopicStore ) AddLastTopic ( item * Topic , fid int ) error {
2017-06-13 07:12:58 +00:00
// Coming Soon...
return nil
}
2017-09-15 22:20:01 +00:00
// Return the total number of topics on these forums
func ( sts * SQLTopicStore ) GetGlobalCount ( ) int {
var tcount int
err := sts . topicCount . QueryRow ( ) . Scan ( & tcount )
if err != nil {
LogError ( err )
}
return tcount
2017-06-13 07:12:58 +00:00
}