2017-12-10 03:43:30 +00:00
|
|
|
package common
|
|
|
|
|
|
|
|
import (
|
|
|
|
"database/sql"
|
2017-12-19 03:53:13 +00:00
|
|
|
"sync"
|
2017-12-10 03:43:30 +00:00
|
|
|
"sync/atomic"
|
|
|
|
|
|
|
|
"../query_gen/lib"
|
|
|
|
)
|
|
|
|
|
2018-01-14 12:03:20 +00:00
|
|
|
// Global counters
|
|
|
|
var GlobalViewCounter *DefaultViewCounter
|
2018-01-09 07:39:29 +00:00
|
|
|
var AgentViewCounter *DefaultAgentViewCounter
|
2017-12-24 07:38:46 +00:00
|
|
|
var RouteViewCounter *DefaultRouteViewCounter
|
2018-01-14 12:03:20 +00:00
|
|
|
var PostCounter *DefaultPostCounter
|
2018-01-18 12:31:25 +00:00
|
|
|
var TopicCounter *DefaultTopicCounter
|
2018-01-14 12:03:20 +00:00
|
|
|
|
|
|
|
// Local counters
|
2017-12-24 07:38:46 +00:00
|
|
|
var TopicViewCounter *DefaultTopicViewCounter
|
2017-12-10 03:43:30 +00:00
|
|
|
|
2018-01-14 12:03:20 +00:00
|
|
|
type DefaultViewCounter struct {
|
2017-12-10 03:43:30 +00:00
|
|
|
buckets [2]int64
|
|
|
|
currentBucket int64
|
|
|
|
|
|
|
|
insert *sql.Stmt
|
|
|
|
}
|
|
|
|
|
2018-01-14 12:03:20 +00:00
|
|
|
func NewGlobalViewCounter() (*DefaultViewCounter, error) {
|
2017-12-10 03:43:30 +00:00
|
|
|
acc := qgen.Builder.Accumulator()
|
2018-01-14 12:03:20 +00:00
|
|
|
counter := &DefaultViewCounter{
|
2017-12-10 03:43:30 +00:00
|
|
|
currentBucket: 0,
|
2017-12-19 03:53:13 +00:00
|
|
|
insert: acc.Insert("viewchunks").Columns("count, createdAt").Fields("?,UTC_TIMESTAMP()").Prepare(),
|
2017-12-10 03:43:30 +00:00
|
|
|
}
|
2017-12-19 03:53:13 +00:00
|
|
|
AddScheduledFifteenMinuteTask(counter.Tick) // This is run once every fifteen minutes to match the frequency of the RouteViewCounter
|
|
|
|
//AddScheduledSecondTask(counter.Tick)
|
2017-12-24 22:08:35 +00:00
|
|
|
AddShutdownTask(counter.Tick)
|
2017-12-10 03:43:30 +00:00
|
|
|
return counter, acc.FirstError()
|
|
|
|
}
|
|
|
|
|
2018-01-14 12:03:20 +00:00
|
|
|
func (counter *DefaultViewCounter) Tick() (err error) {
|
2017-12-10 03:43:30 +00:00
|
|
|
var oldBucket = counter.currentBucket
|
2017-12-22 03:32:23 +00:00
|
|
|
var nextBucket int64 // 0
|
|
|
|
if counter.currentBucket == 0 {
|
2017-12-10 03:43:30 +00:00
|
|
|
nextBucket = 1
|
|
|
|
}
|
|
|
|
atomic.AddInt64(&counter.buckets[oldBucket], counter.buckets[nextBucket])
|
|
|
|
atomic.StoreInt64(&counter.buckets[nextBucket], 0)
|
|
|
|
atomic.StoreInt64(&counter.currentBucket, nextBucket)
|
|
|
|
|
|
|
|
var previousViewChunk = counter.buckets[oldBucket]
|
|
|
|
atomic.AddInt64(&counter.buckets[oldBucket], -previousViewChunk)
|
|
|
|
return counter.insertChunk(previousViewChunk)
|
|
|
|
}
|
|
|
|
|
2018-01-14 12:03:20 +00:00
|
|
|
func (counter *DefaultViewCounter) Bump() {
|
2017-12-10 03:43:30 +00:00
|
|
|
atomic.AddInt64(&counter.buckets[counter.currentBucket], 1)
|
|
|
|
}
|
|
|
|
|
2018-01-14 12:03:20 +00:00
|
|
|
func (counter *DefaultViewCounter) insertChunk(count int64) error {
|
2017-12-10 03:43:30 +00:00
|
|
|
if count == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
debugLogf("Inserting a viewchunk with a count of %d", count)
|
|
|
|
_, err := counter.insert.Exec(count)
|
|
|
|
return err
|
|
|
|
}
|
2017-12-19 03:53:13 +00:00
|
|
|
|
2018-01-14 12:03:20 +00:00
|
|
|
type DefaultPostCounter struct {
|
|
|
|
buckets [2]int64
|
|
|
|
currentBucket int64
|
|
|
|
|
|
|
|
insert *sql.Stmt
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewPostCounter() (*DefaultPostCounter, error) {
|
|
|
|
acc := qgen.Builder.Accumulator()
|
|
|
|
counter := &DefaultPostCounter{
|
|
|
|
currentBucket: 0,
|
|
|
|
insert: acc.Insert("postchunks").Columns("count, createdAt").Fields("?,UTC_TIMESTAMP()").Prepare(),
|
|
|
|
}
|
|
|
|
AddScheduledFifteenMinuteTask(counter.Tick)
|
|
|
|
//AddScheduledSecondTask(counter.Tick)
|
|
|
|
AddShutdownTask(counter.Tick)
|
|
|
|
return counter, acc.FirstError()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (counter *DefaultPostCounter) Tick() (err error) {
|
|
|
|
var oldBucket = counter.currentBucket
|
|
|
|
var nextBucket int64 // 0
|
|
|
|
if counter.currentBucket == 0 {
|
|
|
|
nextBucket = 1
|
|
|
|
}
|
|
|
|
atomic.AddInt64(&counter.buckets[oldBucket], counter.buckets[nextBucket])
|
|
|
|
atomic.StoreInt64(&counter.buckets[nextBucket], 0)
|
|
|
|
atomic.StoreInt64(&counter.currentBucket, nextBucket)
|
|
|
|
|
|
|
|
var previousViewChunk = counter.buckets[oldBucket]
|
|
|
|
atomic.AddInt64(&counter.buckets[oldBucket], -previousViewChunk)
|
|
|
|
return counter.insertChunk(previousViewChunk)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (counter *DefaultPostCounter) Bump() {
|
|
|
|
atomic.AddInt64(&counter.buckets[counter.currentBucket], 1)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (counter *DefaultPostCounter) insertChunk(count int64) error {
|
|
|
|
if count == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
debugLogf("Inserting a postchunk with a count of %d", count)
|
|
|
|
_, err := counter.insert.Exec(count)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-01-18 12:31:25 +00:00
|
|
|
type DefaultTopicCounter struct {
|
|
|
|
buckets [2]int64
|
|
|
|
currentBucket int64
|
|
|
|
|
|
|
|
insert *sql.Stmt
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewTopicCounter() (*DefaultTopicCounter, error) {
|
|
|
|
acc := qgen.Builder.Accumulator()
|
|
|
|
counter := &DefaultTopicCounter{
|
|
|
|
currentBucket: 0,
|
|
|
|
insert: acc.Insert("topicchunks").Columns("count, createdAt").Fields("?,UTC_TIMESTAMP()").Prepare(),
|
|
|
|
}
|
|
|
|
AddScheduledFifteenMinuteTask(counter.Tick)
|
|
|
|
//AddScheduledSecondTask(counter.Tick)
|
|
|
|
AddShutdownTask(counter.Tick)
|
|
|
|
return counter, acc.FirstError()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (counter *DefaultTopicCounter) Tick() (err error) {
|
|
|
|
var oldBucket = counter.currentBucket
|
|
|
|
var nextBucket int64 // 0
|
|
|
|
if counter.currentBucket == 0 {
|
|
|
|
nextBucket = 1
|
|
|
|
}
|
|
|
|
atomic.AddInt64(&counter.buckets[oldBucket], counter.buckets[nextBucket])
|
|
|
|
atomic.StoreInt64(&counter.buckets[nextBucket], 0)
|
|
|
|
atomic.StoreInt64(&counter.currentBucket, nextBucket)
|
|
|
|
|
|
|
|
var previousViewChunk = counter.buckets[oldBucket]
|
|
|
|
atomic.AddInt64(&counter.buckets[oldBucket], -previousViewChunk)
|
|
|
|
return counter.insertChunk(previousViewChunk)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (counter *DefaultTopicCounter) Bump() {
|
|
|
|
atomic.AddInt64(&counter.buckets[counter.currentBucket], 1)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (counter *DefaultTopicCounter) insertChunk(count int64) error {
|
|
|
|
if count == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
debugLogf("Inserting a topicchunk with a count of %d", count)
|
|
|
|
_, err := counter.insert.Exec(count)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2017-12-19 03:53:13 +00:00
|
|
|
type RWMutexCounterBucket struct {
|
|
|
|
counter int
|
|
|
|
sync.RWMutex
|
|
|
|
}
|
|
|
|
|
2018-01-09 07:39:29 +00:00
|
|
|
type DefaultAgentViewCounter struct {
|
|
|
|
agentBuckets []*RWMutexCounterBucket //[AgentID]count
|
|
|
|
insert *sql.Stmt
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewDefaultAgentViewCounter() (*DefaultAgentViewCounter, error) {
|
|
|
|
acc := qgen.Builder.Accumulator()
|
|
|
|
var agentBuckets = make([]*RWMutexCounterBucket, len(agentMapEnum))
|
|
|
|
for bucketID, _ := range agentBuckets {
|
|
|
|
agentBuckets[bucketID] = &RWMutexCounterBucket{counter: 0}
|
|
|
|
}
|
|
|
|
counter := &DefaultAgentViewCounter{
|
|
|
|
agentBuckets: agentBuckets,
|
|
|
|
insert: acc.Insert("viewchunks_agents").Columns("count, createdAt, browser").Fields("?,UTC_TIMESTAMP(),?").Prepare(),
|
|
|
|
}
|
|
|
|
AddScheduledFifteenMinuteTask(counter.Tick)
|
|
|
|
//AddScheduledSecondTask(counter.Tick)
|
|
|
|
AddShutdownTask(counter.Tick)
|
|
|
|
return counter, acc.FirstError()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (counter *DefaultAgentViewCounter) Tick() error {
|
|
|
|
for agentID, agentBucket := range counter.agentBuckets {
|
|
|
|
var count int
|
|
|
|
agentBucket.RLock()
|
|
|
|
count = agentBucket.counter
|
|
|
|
agentBucket.counter = 0
|
|
|
|
agentBucket.RUnlock()
|
|
|
|
|
|
|
|
err := counter.insertChunk(count, agentID) // TODO: Bulk insert for speed?
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (counter *DefaultAgentViewCounter) insertChunk(count int, agent int) error {
|
|
|
|
if count == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
var agentName = reverseAgentMapEnum[agent]
|
|
|
|
debugLogf("Inserting a viewchunk with a count of %d for agent %s (%d)", count, agentName, agent)
|
|
|
|
_, err := counter.insert.Exec(count, agentName)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (counter *DefaultAgentViewCounter) Bump(agent int) {
|
|
|
|
// TODO: Test this check
|
2018-01-14 12:03:20 +00:00
|
|
|
debugDetail("counter.agentBuckets[", agent, "]: ", counter.agentBuckets[agent])
|
2018-01-09 07:39:29 +00:00
|
|
|
if len(counter.agentBuckets) <= agent || agent < 0 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
counter.agentBuckets[agent].Lock()
|
|
|
|
counter.agentBuckets[agent].counter++
|
|
|
|
counter.agentBuckets[agent].Unlock()
|
|
|
|
}
|
|
|
|
|
2017-12-24 07:38:46 +00:00
|
|
|
type DefaultRouteViewCounter struct {
|
2017-12-19 03:53:13 +00:00
|
|
|
routeBuckets []*RWMutexCounterBucket //[RouteID]count
|
|
|
|
insert *sql.Stmt
|
|
|
|
}
|
|
|
|
|
2017-12-24 07:38:46 +00:00
|
|
|
func NewDefaultRouteViewCounter() (*DefaultRouteViewCounter, error) {
|
2017-12-19 03:53:13 +00:00
|
|
|
acc := qgen.Builder.Accumulator()
|
|
|
|
var routeBuckets = make([]*RWMutexCounterBucket, len(routeMapEnum))
|
|
|
|
for bucketID, _ := range routeBuckets {
|
|
|
|
routeBuckets[bucketID] = &RWMutexCounterBucket{counter: 0}
|
|
|
|
}
|
2017-12-24 07:38:46 +00:00
|
|
|
counter := &DefaultRouteViewCounter{
|
2017-12-19 03:53:13 +00:00
|
|
|
routeBuckets: routeBuckets,
|
|
|
|
insert: acc.Insert("viewchunks").Columns("count, createdAt, route").Fields("?,UTC_TIMESTAMP(),?").Prepare(),
|
|
|
|
}
|
|
|
|
AddScheduledFifteenMinuteTask(counter.Tick) // There could be a lot of routes, so we don't want to be running this every second
|
|
|
|
//AddScheduledSecondTask(counter.Tick)
|
2017-12-24 22:08:35 +00:00
|
|
|
AddShutdownTask(counter.Tick)
|
2017-12-19 03:53:13 +00:00
|
|
|
return counter, acc.FirstError()
|
|
|
|
}
|
|
|
|
|
2017-12-24 07:38:46 +00:00
|
|
|
func (counter *DefaultRouteViewCounter) Tick() error {
|
2017-12-19 03:53:13 +00:00
|
|
|
for routeID, routeBucket := range counter.routeBuckets {
|
|
|
|
var count int
|
|
|
|
routeBucket.RLock()
|
|
|
|
count = routeBucket.counter
|
|
|
|
routeBucket.counter = 0
|
|
|
|
routeBucket.RUnlock()
|
|
|
|
|
|
|
|
err := counter.insertChunk(count, routeID) // TODO: Bulk insert for speed?
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-12-24 07:38:46 +00:00
|
|
|
func (counter *DefaultRouteViewCounter) insertChunk(count int, route int) error {
|
2017-12-19 03:53:13 +00:00
|
|
|
if count == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
var routeName = reverseRouteMapEnum[route]
|
|
|
|
debugLogf("Inserting a viewchunk with a count of %d for route %s (%d)", count, routeName, route)
|
|
|
|
_, err := counter.insert.Exec(count, routeName)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2017-12-24 07:38:46 +00:00
|
|
|
func (counter *DefaultRouteViewCounter) Bump(route int) {
|
2017-12-19 03:53:13 +00:00
|
|
|
// TODO: Test this check
|
2017-12-24 22:08:35 +00:00
|
|
|
debugLog("counter.routeBuckets[", route, "]: ", counter.routeBuckets[route])
|
2018-01-09 07:39:29 +00:00
|
|
|
if len(counter.routeBuckets) <= route || route < 0 {
|
2017-12-19 03:53:13 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
counter.routeBuckets[route].Lock()
|
|
|
|
counter.routeBuckets[route].counter++
|
|
|
|
counter.routeBuckets[route].Unlock()
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: The ForumViewCounter and TopicViewCounter
|
|
|
|
|
|
|
|
// TODO: Unload forum counters without any views over the past 15 minutes, if the admin has configured the forumstore with a cap and it's been hit?
|
|
|
|
// Forums can be reloaded from the database at any time, so we want to keep the counters separate from them
|
|
|
|
type ForumViewCounter struct {
|
|
|
|
buckets [2]int64
|
|
|
|
currentBucket int64
|
|
|
|
}
|
|
|
|
|
|
|
|
/*func (counter *ForumViewCounter) insertChunk(count int, forum int) error {
|
|
|
|
if count == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
debugLogf("Inserting a viewchunk with a count of %d for forum %d", count, forum)
|
|
|
|
_, err := counter.insert.Exec(count, forum)
|
|
|
|
return err
|
|
|
|
}*/
|
|
|
|
|
|
|
|
// TODO: Use two odd-even maps for now, and move to something more concurrent later, maybe a sharded map?
|
2017-12-24 07:38:46 +00:00
|
|
|
type DefaultTopicViewCounter struct {
|
2017-12-19 03:53:13 +00:00
|
|
|
oddTopics map[int]*RWMutexCounterBucket // map[tid]struct{counter,sync.RWMutex}
|
|
|
|
evenTopics map[int]*RWMutexCounterBucket
|
|
|
|
oddLock sync.RWMutex
|
|
|
|
evenLock sync.RWMutex
|
|
|
|
|
|
|
|
update *sql.Stmt
|
|
|
|
}
|
|
|
|
|
2017-12-24 07:38:46 +00:00
|
|
|
func NewDefaultTopicViewCounter() (*DefaultTopicViewCounter, error) {
|
2017-12-19 03:53:13 +00:00
|
|
|
acc := qgen.Builder.Accumulator()
|
2017-12-24 07:38:46 +00:00
|
|
|
counter := &DefaultTopicViewCounter{
|
2017-12-19 03:53:13 +00:00
|
|
|
oddTopics: make(map[int]*RWMutexCounterBucket),
|
|
|
|
evenTopics: make(map[int]*RWMutexCounterBucket),
|
2017-12-24 07:38:46 +00:00
|
|
|
update: acc.Update("topics").Set("views = views + ?").Where("tid = ?").Prepare(),
|
2017-12-19 03:53:13 +00:00
|
|
|
}
|
2017-12-24 22:08:35 +00:00
|
|
|
AddScheduledFifteenMinuteTask(counter.Tick) // Who knows how many topics we have queued up, we probably don't want this running too frequently
|
2017-12-19 03:53:13 +00:00
|
|
|
//AddScheduledSecondTask(counter.Tick)
|
2017-12-24 22:08:35 +00:00
|
|
|
AddShutdownTask(counter.Tick)
|
2017-12-19 03:53:13 +00:00
|
|
|
return counter, acc.FirstError()
|
|
|
|
}
|
|
|
|
|
2017-12-24 07:38:46 +00:00
|
|
|
func (counter *DefaultTopicViewCounter) Tick() error {
|
2017-12-19 03:53:13 +00:00
|
|
|
counter.oddLock.RLock()
|
2017-12-24 22:08:35 +00:00
|
|
|
oddTopics := counter.oddTopics
|
|
|
|
counter.oddLock.RUnlock()
|
|
|
|
for topicID, topic := range oddTopics {
|
2017-12-19 03:53:13 +00:00
|
|
|
var count int
|
|
|
|
topic.RLock()
|
|
|
|
count = topic.counter
|
|
|
|
topic.RUnlock()
|
2017-12-24 22:08:35 +00:00
|
|
|
// TODO: Only delete the bucket when it's zero to avoid hitting popular topics?
|
|
|
|
counter.oddLock.Lock()
|
|
|
|
delete(counter.oddTopics, topicID)
|
|
|
|
counter.oddLock.Unlock()
|
2017-12-19 03:53:13 +00:00
|
|
|
err := counter.insertChunk(count, topicID)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
counter.evenLock.RLock()
|
2017-12-24 22:08:35 +00:00
|
|
|
evenTopics := counter.evenTopics
|
|
|
|
counter.evenLock.RUnlock()
|
|
|
|
for topicID, topic := range evenTopics {
|
2017-12-19 03:53:13 +00:00
|
|
|
var count int
|
|
|
|
topic.RLock()
|
|
|
|
count = topic.counter
|
|
|
|
topic.RUnlock()
|
2017-12-24 22:08:35 +00:00
|
|
|
// TODO: Only delete the bucket when it's zero to avoid hitting popular topics?
|
|
|
|
counter.evenLock.Lock()
|
|
|
|
delete(counter.evenTopics, topicID)
|
|
|
|
counter.evenLock.Unlock()
|
2017-12-19 03:53:13 +00:00
|
|
|
err := counter.insertChunk(count, topicID)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-12-24 22:08:35 +00:00
|
|
|
// 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.
|
2017-12-24 07:38:46 +00:00
|
|
|
func (counter *DefaultTopicViewCounter) insertChunk(count int, topicID int) error {
|
2017-12-19 03:53:13 +00:00
|
|
|
if count == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
debugLogf("Inserting %d views into topic %d", count, topicID)
|
|
|
|
_, err := counter.update.Exec(count, topicID)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2017-12-24 07:38:46 +00:00
|
|
|
func (counter *DefaultTopicViewCounter) Bump(topicID int) {
|
2017-12-19 03:53:13 +00:00
|
|
|
// Is the ID even?
|
|
|
|
if topicID%2 == 0 {
|
2017-12-24 22:08:35 +00:00
|
|
|
counter.evenLock.RLock()
|
2017-12-19 03:53:13 +00:00
|
|
|
topic, ok := counter.evenTopics[topicID]
|
2017-12-24 22:08:35 +00:00
|
|
|
counter.evenLock.RUnlock()
|
2017-12-19 03:53:13 +00:00
|
|
|
if ok {
|
|
|
|
topic.Lock()
|
|
|
|
topic.counter++
|
|
|
|
topic.Unlock()
|
|
|
|
} else {
|
|
|
|
counter.evenLock.Lock()
|
|
|
|
counter.evenTopics[topicID] = &RWMutexCounterBucket{counter: 1}
|
|
|
|
counter.evenLock.Unlock()
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-12-24 22:08:35 +00:00
|
|
|
counter.oddLock.RLock()
|
2017-12-19 03:53:13 +00:00
|
|
|
topic, ok := counter.oddTopics[topicID]
|
2017-12-24 22:08:35 +00:00
|
|
|
counter.oddLock.RUnlock()
|
2017-12-19 03:53:13 +00:00
|
|
|
if ok {
|
|
|
|
topic.Lock()
|
|
|
|
topic.counter++
|
|
|
|
topic.Unlock()
|
|
|
|
} else {
|
|
|
|
counter.oddLock.Lock()
|
|
|
|
counter.oddTopics[topicID] = &RWMutexCounterBucket{counter: 1}
|
|
|
|
counter.oddLock.Unlock()
|
|
|
|
}
|
|
|
|
}
|