Решение на ExpireMap от Илия Ячев

Обратно към всички решения

Към профила на Илия Ячев

Резултати

  • 9 точки от тестове
  • 0 бонус точки
  • 9 точки общо
  • 22 успешни тест(а)
  • 3 неуспешни тест(а)

Код

package main
import (
"strconv"
"strings"
"time"
)
type ExpireMap struct {
expiredChannel chan string
expires map[string]time.Time
data map[string]interface{}
mutex chan struct{}
stopChannels map[string]chan struct{}
}
type ExpireMapError struct {
cause string
}
func (e *ExpireMapError) Error() string {
return e.cause
}
// Връща указател към нов обект от тип ExpireMap, готов за използване.
func NewExpireMap() *ExpireMap {
expiredChannel := make(chan string, 1)
expires := map[string]time.Time{}
data := map[string]interface{}{}
mutex := make(chan struct{}, 1)
stopChannels := map[string]chan struct{}{}
return &ExpireMap{expiredChannel, expires, data, mutex, stopChannels}
}
func (em *ExpireMap) lock() {
em.mutex <- struct{}{}
}
func (em *ExpireMap) unlock() {
<-em.mutex
}
// Requires lock
func (em *ExpireMap) stopGoroutine(key string) {
if ch, ok := em.stopChannels[key]; ok {
ch <- struct{}{}
}
}
// Добавя в хранилището нов ключ key със стойност value за expire време. Duration е дефиниран в пакета time. expire време след добавянето на ключа той вече трябва да спре да бъде в хранилището. Стойността може да е от всякакъв тип, докато ключът е от тип string.
func (em *ExpireMap) Set(key string, value interface{}, expire time.Duration) {
em.lock()
defer em.unlock()
em.stopGoroutine(key)
em.stopChannels[key] = make(chan struct{})
em.data[key] = value
em.expires[key] = time.Now().Add(expire)
go func(stop chan struct{}) {
select {
case <-stop:
case <-time.After(expire):
em.lock()
em.delete(key)
em.unlock()
select {
case em.expiredChannel <- key:
default:
}
}
}(em.stopChannels[key])
}
// Връща стойността на ключ и true, когато ключът е намерен. Когато ключът липсва или е изтекъл връща nil и false.
func (em *ExpireMap) Get(key string) (v interface{}, ok bool) {
v, ok = em.data[key]
return
}
// Връща стойността, отговаряща на този ключ и true, когато ключът е намерен и стойността му е от тип int. Когато ключът липсва, изтекъл е или стойността му не е от този тип методът трябва да върне нула и false.
func (em *ExpireMap) GetInt(key string) (v int, ok bool) {
v, ok = em.data[key].(int)
return
}
// Връща стойността на този ключ и true, когато ключът е намерен и стойността му е от тип float64. Когато ключът липсва, изтекъл е или стойността му не е от този тип методът трябва да върне нула и false.
func (em *ExpireMap) GetFloat64(key string) (v float64, ok bool) {
v, ok = em.data[key].(float64)
return
}
// Връща стойността на ключ и true, когато ключът е намерен и стойността му е от тип string. Когато ключът липсва, изтекъл е или стойността не е от тип string методът трябва да върне празен стринг и false.
func (em *ExpireMap) GetString(key string) (v string, ok bool) {
v, ok = em.data[key].(string)
return
}
// Връща стойността на ключ и true, когато ключът е намерен и стойността му е от тип bool. Когато ключът липсва, изтекъл е или стойността не е от тип bool методът трябва да върне false и false.
func (em *ExpireMap) GetBool(key string) (v bool, ok bool) {
v, ok = em.data[key].(bool)
return
}
// За определен ключ тази функция трябва да върне времето, когато той ще изтече. Втората върната стойност е true когато ключът е намерен в хранилището. Когато не е намерен функцията трябва да върне нулевото време и false.
func (em *ExpireMap) Expires(key string) (v time.Time, ok bool) {
v, ok = em.expires[key]
return
}
// Requires lock
func (em *ExpireMap) delete(key string) {
delete(em.data, key)
delete(em.expires, key)
delete(em.stopChannels, key)
}
// Премахва ключа от хранилището. Когато няма такъв ключ методът не трябва да гърми.
func (em *ExpireMap) Delete(key string) {
em.lock()
defer em.unlock()
em.stopGoroutine(key)
em.delete(key)
}
// Казва дали ключ е в хранилището или не. Отново, ключове са в хранилището само ако техния expire не е вече изтекъл.
func (em *ExpireMap) Contains(key string) (ok bool) {
_, ok = em.Get(key)
return
}
// Връща големината на хранилището. Големина е бройката неизтекли ключове, които са в него.
func (em *ExpireMap) Size() int {
return len(em.data)
}
func (em *ExpireMap) addValueToIntOrString(key string, delta int) error {
em.lock()
defer em.unlock()
if v, ok := em.GetInt(key); ok {
em.data[key] = v + delta
} else if v, ok := em.GetString(key); ok {
intV, ok := strconv.Atoi(v)
if ok != nil {
return ok
}
em.data[key] = strconv.Itoa(intV + delta)
} else {
return &ExpireMapError{"Could not find int or string value at " + key}
}
return nil
}
// За ключ увеличава стойността му числово с 1 когато тази стойност е от тип int или стринг, представящ цяло число. Когато стойността е от тип, различен от тези неща функцията трябва да върне грешка, различна от nil. float32 и float64 не са целочислен типове. Времето на изтичане на ключа не трябва да се променя.
func (em *ExpireMap) Increment(key string) error {
return em.addValueToIntOrString(key, 1)
}
// Същото като Increment, но намалява стойността с 1 вместо да я увеличава.
func (em *ExpireMap) Decrement(key string) error {
return em.addValueToIntOrString(key, -1)
}
// Ако стойността за ключа key е от тип string, то всички букви в този string трябва да станат главни. При успех методът трябва да върне nil, а при невъзможност да изпълни задачата - грешка.
func (em *ExpireMap) ToUpper(key string) (ok error) {
em.lock()
defer em.unlock()
if v, ok := em.GetString(key); ok {
em.data[key] = strings.ToUpper(v)
}
return
}
func (em *ExpireMap) ToLower(key string) (ok error) {
em.lock()
defer em.unlock()
if v, ok := em.GetString(key); ok {
em.data[key] = strings.ToLower(v)
}
return
}
// Тази функция трябва да върне канал, по който да идват ключовете, които са изтекли. Те трябва да се пускат по канала в момента на изтичането си. Ключове, които са изтрити с Delete или Cleanup не трябва да се връщат по този канал. Единствено тези, които са достигнали до времето си на изтичане. Не е задължително през цялото време някой да чете от върнатия канал. Ключове добавени в инстанцията след извикването на ExpireChan също трябва да се пратят по канала, когато тяхното време дойде.
func (em *ExpireMap) ExpiredChan() <-chan string {
return em.expiredChannel
}
// Изчиства хранилището. След извикване на функцията в ExpireMap-a не трябва да се съдържа нито един ключ.
func (em *ExpireMap) Cleanup() {
em.lock()
defer em.unlock()
for key := range em.data {
em.stopGoroutine(key)
em.delete(key)
}
}
// Трябва да освободи всички ресурси, които е използвал този ExpireMap. Това означава изчистване на всички структури, затваряне на всички канали и спиране на всички горутини, свързани с нормалната му работа. След извикване на Destroy тази инстанция на ExpireMap няма да бъде повече използвана. Този метод би трябвало да се ползва по подобен начин:
func (em *ExpireMap) Destroy() {
em.lock()
for key := range em.data {
em.stopGoroutine(key)
}
em.data = nil
em.expires = nil
em.stopChannels = nil
close(em.expiredChannel)
close(em.mutex)
}

Лог от изпълнението

PASS
ok  	_/tmp/d20141204-6466-10cyed2	0.012s
PASS
ok  	_/tmp/d20141204-6466-10cyed2	0.012s
panic: test timed out

goroutine 15 [running]:
testing.alarm()
	/usr/local/lib/go/src/pkg/testing/testing.go:533 +0x44
created by time.goFunc
	/usr/local/lib/go/src/pkg/time/sleep.go:122 +0x45

goroutine 1 [chan receive]:
testing.RunTests(0x8147e94, 0x81c2ba0, 0x19, 0x19, 0x1, ...)
	/usr/local/lib/go/src/pkg/testing/testing.go:434 +0x69f
testing.Main(0x8147e94, 0x81c2ba0, 0x19, 0x19, 0x81c5600, ...)
	/usr/local/lib/go/src/pkg/testing/testing.go:365 +0x69
main.main()
	_/tmp/d20141204-6466-10cyed2/_test/_testmain.go:91 +0x81

goroutine 4 [sleep]:
time.Sleep(0x3c336080, 0x0)
	/usr/local/lib/go/src/pkg/runtime/ztime_linux_386.c:19 +0x3a
_/tmp/d20141204-6466-10cyed2.TestSizes(0x1837a1e0)
	/tmp/d20141204-6466-10cyed2/solution_test.go:111 +0x337
testing.tRunner(0x1837a1e0, 0x81c2bb8)
	/usr/local/lib/go/src/pkg/testing/testing.go:353 +0x87
created by testing.RunTests
	/usr/local/lib/go/src/pkg/testing/testing.go:433 +0x684

goroutine 5 [select]:
_/tmp/d20141204-6466-10cyed2.func·001(0x18348630)
	/tmp/d20141204-6466-10cyed2/solution.go:60 +0x1b5
created by _/tmp/d20141204-6466-10cyed2.(*ExpireMap).Set
	/tmp/d20141204-6466-10cyed2/solution.go:71 +0x278

goroutine 6 [select]:
_/tmp/d20141204-6466-10cyed2.func·001(0x18348660)
	/tmp/d20141204-6466-10cyed2/solution.go:60 +0x1b5
created by _/tmp/d20141204-6466-10cyed2.(*ExpireMap).Set
	/tmp/d20141204-6466-10cyed2/solution.go:71 +0x278

goroutine 7 [select]:
_/tmp/d20141204-6466-10cyed2.func·001(0x18348690)
	/tmp/d20141204-6466-10cyed2/solution.go:60 +0x1b5
created by _/tmp/d20141204-6466-10cyed2.(*ExpireMap).Set
	/tmp/d20141204-6466-10cyed2/solution.go:71 +0x278

goroutine 8 [select]:
_/tmp/d20141204-6466-10cyed2.func·001(0x183486c0)
	/tmp/d20141204-6466-10cyed2/solution.go:60 +0x1b5
created by _/tmp/d20141204-6466-10cyed2.(*ExpireMap).Set
	/tmp/d20141204-6466-10cyed2/solution.go:71 +0x278

goroutine 9 [select]:
_/tmp/d20141204-6466-10cyed2.func·001(0x183486f0)
	/tmp/d20141204-6466-10cyed2/solution.go:60 +0x1b5
created by _/tmp/d20141204-6466-10cyed2.(*ExpireMap).Set
	/tmp/d20141204-6466-10cyed2/solution.go:71 +0x278

goroutine 10 [select]:
_/tmp/d20141204-6466-10cyed2.func·001(0x18348720)
	/tmp/d20141204-6466-10cyed2/solution.go:60 +0x1b5
created by _/tmp/d20141204-6466-10cyed2.(*ExpireMap).Set
	/tmp/d20141204-6466-10cyed2/solution.go:71 +0x278

goroutine 11 [select]:
_/tmp/d20141204-6466-10cyed2.func·001(0x18348750)
	/tmp/d20141204-6466-10cyed2/solution.go:60 +0x1b5
created by _/tmp/d20141204-6466-10cyed2.(*ExpireMap).Set
	/tmp/d20141204-6466-10cyed2/solution.go:71 +0x278

goroutine 12 [select]:
_/tmp/d20141204-6466-10cyed2.func·001(0x18348780)
	/tmp/d20141204-6466-10cyed2/solution.go:60 +0x1b5
created by _/tmp/d20141204-6466-10cyed2.(*ExpireMap).Set
	/tmp/d20141204-6466-10cyed2/solution.go:71 +0x278

goroutine 13 [select]:
_/tmp/d20141204-6466-10cyed2.func·001(0x183487b0)
	/tmp/d20141204-6466-10cyed2/solution.go:60 +0x1b5
created by _/tmp/d20141204-6466-10cyed2.(*ExpireMap).Set
	/tmp/d20141204-6466-10cyed2/solution.go:71 +0x278

goroutine 14 [select]:
_/tmp/d20141204-6466-10cyed2.func·001(0x183487e0)
	/tmp/d20141204-6466-10cyed2/solution.go:60 +0x1b5
created by _/tmp/d20141204-6466-10cyed2.(*ExpireMap).Set
	/tmp/d20141204-6466-10cyed2/solution.go:71 +0x278
exit status 2
FAIL	_/tmp/d20141204-6466-10cyed2	1.012s
PASS
ok  	_/tmp/d20141204-6466-10cyed2	0.122s
PASS
ok  	_/tmp/d20141204-6466-10cyed2	0.172s
PASS
ok  	_/tmp/d20141204-6466-10cyed2	0.012s
PASS
ok  	_/tmp/d20141204-6466-10cyed2	0.012s
PASS
ok  	_/tmp/d20141204-6466-10cyed2	0.012s
PASS
ok  	_/tmp/d20141204-6466-10cyed2	0.012s
PASS
ok  	_/tmp/d20141204-6466-10cyed2	0.051s
PASS
ok  	_/tmp/d20141204-6466-10cyed2	0.012s
PASS
ok  	_/tmp/d20141204-6466-10cyed2	0.012s
PASS
ok  	_/tmp/d20141204-6466-10cyed2	0.012s
PASS
ok  	_/tmp/d20141204-6466-10cyed2	0.012s
PASS
ok  	_/tmp/d20141204-6466-10cyed2	0.112s
PASS
ok  	_/tmp/d20141204-6466-10cyed2	0.062s
--- FAIL: TestExpiredChanWhenNoOneIsReading-2 (0.06 seconds)
	solution_test.go:554: Wrong key expired
FAIL
exit status 1
FAIL	_/tmp/d20141204-6466-10cyed2	0.072s
PASS
ok  	_/tmp/d20141204-6466-10cyed2	0.112s
PASS
ok  	_/tmp/d20141204-6466-10cyed2	0.132s
PASS
ok  	_/tmp/d20141204-6466-10cyed2	0.172s
PASS
ok  	_/tmp/d20141204-6466-10cyed2	0.012s
PASS
ok  	_/tmp/d20141204-6466-10cyed2	0.012s
PASS
ok  	_/tmp/d20141204-6466-10cyed2	0.012s
PASS
ok  	_/tmp/d20141204-6466-10cyed2	0.272s
--- FAIL: TestStringMethods-2 (0.00 seconds)
	solution_test.go:948: ToUpper for key did not return an error
	solution_test.go:952: ToLower for key did not return an error
	solution_test.go:948: ToUpper for key did not return an error
	solution_test.go:952: ToLower for key did not return an error
FAIL
exit status 1
FAIL	_/tmp/d20141204-6466-10cyed2	0.021s

История (2 версии и 3 коментара)

Илия обнови решението на 17.11.2014 01:03 (преди над 3 години)

+package main
+
+import (
+ "strconv"
+ "strings"
+ "time"
+)
+
+type ExpireMap struct {
+ channel chan string
+ expires map[string]time.Time
+ data map[string]interface{}
+ m chan struct{}
+}
+
+type ExpireMapError struct {
+ cause string
+}
+
+func (e *ExpireMapError) Error() string {
+ return e.cause
+}
+
+// Връща указател към нов обект от тип ExpireMap, готов за използване.
+func NewExpireMap() *ExpireMap {
+ channel := make(chan string, 1)
+ expires := map[string]time.Time{}
+ data := map[string]interface{}{}
+ m := make(chan struct{}, 1)
+ return &ExpireMap{channel, expires, data, m}
+}
+
+func (em *ExpireMap) lock() {
+ em.m <- struct{}{}
+}
+func (em *ExpireMap) unlock() {
+ <-em.m
+}
+
+// Добавя в хранилището нов ключ key със стойност value за expire време. Duration е дефиниран в пакета time. expire време след добавянето на ключа той вече трябва да спре да бъде в хранилището. Стойността може да е от всякакъв тип, докато ключа е от тип string.
+func (em *ExpireMap) Set(key string, value interface{}, expire time.Duration) {
+ em.lock()
+ defer em.unlock()
+ em.data[key] = value
+ em.expires[key] = time.Now().Add(expire)
+ go func() {
+ time.Sleep(expire)
+ em.Delete(key)
+ select {
+ case em.channel <- key:
+ default:
+ }
+ }()
+}
+
+// Връща стойността на ключ и true, когато ключа е намерен. Когато ключа липсва или е изтекъл връща nil и false.
+func (em *ExpireMap) Get(key string) (v interface{}, ok bool) {
+ v, ok = em.data[key]
+ return
+}
+
+// Връща стойността, отговаряща на този ключ и true, когато ключа е намерен и стойността му е от тип int. Когато ключа липсва, изтекъл е или стойността му не е от този тип метода трябва да върне нула и false.
+func (em *ExpireMap) GetInt(key string) (v int, ok bool) {
+ v, ok = em.data[key].(int)
+ return
+}
+
+// Връща стойността на този ключ и true, когато ключа е намерен и стойността му е от тип float64. Когато ключа липсва, изтекъл е или стойността му не е от този тип метода трябва да върне нула и false.
+func (em *ExpireMap) GetFloat64(key string) (v float64, ok bool) {
+ v, ok = em.data[key].(float64)
+ return
+}
+
+// Връща стойността на ключ и true, когато ключа е намерен и стойността му е от тип string. Когато ключа липсва, изтекъл е или стойността не е от тип string метода трябва да върне празен стринг и false.
+func (em *ExpireMap) GetString(key string) (v string, ok bool) {
+ v, ok = em.data[key].(string)
+ return
+}
+
+// Връща стойността на ключ и true, когато ключа е намерен и стойността му е от тип bool. Когато ключа липсва, изтекъл е или стойността не е от тип bool метода трябва да върне false и false.
+func (em *ExpireMap) GetBool(key string) (v bool, ok bool) {
+ v, ok = em.data[key].(bool)
+ return
+}
+
+// За определен ключ тази функция трябва да върне времето, когато той ще изтече. Втората върната стойност е true когато ключа е намерен в хранилището. Когато не е намерен функцията трябва да върне нулевото време и false.
+func (em *ExpireMap) Expires(key string) (v time.Time, ok bool) {
+ v, ok = em.expires[key]
+ return
+}
+
+// Премахва ключа от хранилището. Когато няма такъв ключ метода не трябав да гърми.
+func (em *ExpireMap) Delete(key string) {
+ em.lock()
+ defer em.unlock()
+ delete(em.data, key)
+ delete(em.expires, key)
+}
+
+// Казва дали ключ е в хранилището или не. Отново, ключове са в хранилището само ако техния expire не е вече изтекъл.
+func (em *ExpireMap) Contains(key string) (ok bool) {
+ _, ok = em.Get(key)
+ return
+}
+
+// Връща големината на хранилището. Големина е бройката неизтекли ключове, които са в него.
+func (em *ExpireMap) Size() int {
+ return len(em.data)
+}
+
+func (em *ExpireMap) addValueToIntOrString(key string, delta int) error {
+ em.lock()
+ defer em.unlock()
+ if !em.Contains(key) {
+ return nil
+ }
+ if v, ok := em.GetInt(key); ok {
+ em.data[key] = v + delta
+ } else if v, ok := em.GetString(key); ok {
+ intV, ok := strconv.Atoi(v)
+ if ok != nil {
+ return ok
+ }
+ em.data[key] = strconv.Itoa(intV + delta)
+ } else {
+ return &ExpireMapError{""}
+ }
+ return nil
+}
+
+// За ключ увеличава стойността му числово с 1 когато тази стойност е от тип int или стринг, представящ цяло число. Когато стойността е от тип, различен от тези неща функцията трябва да върне грешка, различна от nil. float32 и float64 не са целочислен типове. Времето на изтичане на ключа не трябва да се променя.
+func (em *ExpireMap) Increment(key string) error {
+ return em.addValueToIntOrString(key, 1)
+}
+
+// Същото като Increment, но намалява стойността с 1 вместо да я увеличава.
+func (em *ExpireMap) Decrement(key string) error {
+ return em.addValueToIntOrString(key, -1)
+}
+
+// Ако стойността за ключа key е от тип string, то всички букви в този string трябва да станат главни. При успех метода трябва да върне nil, а при невъзможност да изпълни задачата - грешка.
+func (em *ExpireMap) ToUpper(key string) (ok error) {
+ em.lock()
+ defer em.unlock()
+ if v, ok := em.GetString(key); ok {
+ em.data[key] = strings.ToUpper(v)
+ }
+ return
+}
+
+func (em *ExpireMap) ToLower(key string) (ok error) {
+ em.lock()
+ defer em.unlock()
+ if v, ok := em.GetString(key); ok {
+ em.data[key] = strings.ToLower(v)
+ }
+ return
+}
+
+// Тази функция трябва да върне канал, по който да идват ключовете, които са изтекли. Те трябва да се пускат по канала в момента на изтичането си. Ключове, които са изтрити с Delete или Cleanup не трябва да се връщат по този канал. Единствено тези, които са достигнали до времето си на изтичане. Не е задължително през цялото време някой да чете от върнатия канал. Ключове добавени в инстанцията след извикването на ExpireChan също трябва да се пратят по канала, когато тяхното време дойде.
+func (em *ExpireMap) ExpiredChan() <-chan string {
+ return em.channel
+}
+
+// Изчиства хранилището. След извикване на функцията в ExpireMap-a не трябва да се съдържа нито един ключ.
+func (em *ExpireMap) Cleanup() {
+ em.lock()
+ defer em.unlock()
+ for k := range em.data {
+ delete(em.data, k)
+ }
+ for k := range em.expires {
+ delete(em.expires, k)
+ }
+}
+
+// Трябва да освободи всички ресурси, които е използвал този ExpireMap. Това означава изчистване на всички структури, затваряне на всички канали и спиране на всички горутини, свързани с нормалната му работа. След извикване на Destroy тази инстанция на ExpireMap няма да бъде повече използвана. Този метод би трябвало да се ползва по подобен начин:
+func (em *ExpireMap) Destroy() {
+ em.lock()
+ em.data = nil
+ em.expires = nil
+ close(em.channel)
+ close(em.m)
+ // TODO: close goroutines
+}

Браво че си се сетил че може и никой да не чете на expiredchan-а

Забележки:

  • Ако връщаме грешка че даден ключ не може да се увеличи защото стойността му не е от правилния тип, дали трябва да връщаме грешка ако го няма ?

  • Пусни си мапа да работи по дълго предлагам следния код:

em.Set("foo", 5, time.Millisecond)
em.Destroy()
time.Sleep(time.Second)
// panic here 

Мога още но се надявам че след като оправиш тези и махнеш // TODO: close goroutines няма да има нужда :)

Илия обнови решението на 18.11.2014 00:10 (преди над 3 години)

package main
import (
"strconv"
"strings"
"time"
)
type ExpireMap struct {
- channel chan string
- expires map[string]time.Time
- data map[string]interface{}
- m chan struct{}
+ expiredChannel chan string
+ expires map[string]time.Time
+ data map[string]interface{}
+ mutex chan struct{}
+ stopChannels map[string]chan struct{}
}
type ExpireMapError struct {
cause string
}
func (e *ExpireMapError) Error() string {
return e.cause
}
// Връща указател към нов обект от тип ExpireMap, готов за използване.
func NewExpireMap() *ExpireMap {
- channel := make(chan string, 1)
+ expiredChannel := make(chan string, 1)
expires := map[string]time.Time{}
data := map[string]interface{}{}
- m := make(chan struct{}, 1)
- return &ExpireMap{channel, expires, data, m}
+ mutex := make(chan struct{}, 1)
+ stopChannels := map[string]chan struct{}{}
+ return &ExpireMap{expiredChannel, expires, data, mutex, stopChannels}
}
func (em *ExpireMap) lock() {
- em.m <- struct{}{}
+ em.mutex <- struct{}{}
}
func (em *ExpireMap) unlock() {
- <-em.m
+ <-em.mutex
}
-// Добавя в хранилището нов ключ key със стойност value за expire време. Duration е дефиниран в пакета time. expire време след добавянето на ключа той вече трябва да спре да бъде в хранилището. Стойността може да е от всякакъв тип, докато ключа е от тип string.
+// Requires lock
+func (em *ExpireMap) stopGoroutine(key string) {
+ if ch, ok := em.stopChannels[key]; ok {
+ ch <- struct{}{}
+ }
+}
+
+// Добавя в хранилището нов ключ key със стойност value за expire време. Duration е дефиниран в пакета time. expire време след добавянето на ключа той вече трябва да спре да бъде в хранилището. Стойността може да е от всякакъв тип, докато ключът е от тип string.
func (em *ExpireMap) Set(key string, value interface{}, expire time.Duration) {
em.lock()
defer em.unlock()
+
+ em.stopGoroutine(key)
+ em.stopChannels[key] = make(chan struct{})
em.data[key] = value
em.expires[key] = time.Now().Add(expire)
- go func() {
- time.Sleep(expire)
- em.Delete(key)
+
+ go func(stop chan struct{}) {
select {
- case em.channel <- key:
- default:
+ case <-stop:
+ case <-time.After(expire):
+ em.lock()
+ em.delete(key)
+ em.unlock()
+ select {
+ case em.expiredChannel <- key:
+ default:
+ }
}
- }()
+ }(em.stopChannels[key])
}
-// Връща стойността на ключ и true, когато ключа е намерен. Когато ключа липсва или е изтекъл връща nil и false.
+// Връща стойността на ключ и true, когато ключът е намерен. Когато ключът липсва или е изтекъл връща nil и false.
func (em *ExpireMap) Get(key string) (v interface{}, ok bool) {
v, ok = em.data[key]
return
}
-// Връща стойността, отговаряща на този ключ и true, когато ключа е намерен и стойността му е от тип int. Когато ключа липсва, изтекъл е или стойността му не е от този тип метода трябва да върне нула и false.
+// Връща стойността, отговаряща на този ключ и true, когато ключът е намерен и стойността му е от тип int. Когато ключът липсва, изтекъл е или стойността му не е от този тип методът трябва да върне нула и false.
func (em *ExpireMap) GetInt(key string) (v int, ok bool) {
v, ok = em.data[key].(int)
return
}
-// Връща стойността на този ключ и true, когато ключа е намерен и стойността му е от тип float64. Когато ключа липсва, изтекъл е или стойността му не е от този тип метода трябва да върне нула и false.
+// Връща стойността на този ключ и true, когато ключът е намерен и стойността му е от тип float64. Когато ключът липсва, изтекъл е или стойността му не е от този тип методът трябва да върне нула и false.
func (em *ExpireMap) GetFloat64(key string) (v float64, ok bool) {
v, ok = em.data[key].(float64)
return
}
-// Връща стойността на ключ и true, когато ключа е намерен и стойността му е от тип string. Когато ключа липсва, изтекъл е или стойността не е от тип string метода трябва да върне празен стринг и false.
+// Връща стойността на ключ и true, когато ключът е намерен и стойността му е от тип string. Когато ключът липсва, изтекъл е или стойността не е от тип string методът трябва да върне празен стринг и false.
func (em *ExpireMap) GetString(key string) (v string, ok bool) {
v, ok = em.data[key].(string)
return
}
-// Връща стойността на ключ и true, когато ключа е намерен и стойността му е от тип bool. Когато ключа липсва, изтекъл е или стойността не е от тип bool метода трябва да върне false и false.
+// Връща стойността на ключ и true, когато ключът е намерен и стойността му е от тип bool. Когато ключът липсва, изтекъл е или стойността не е от тип bool методът трябва да върне false и false.
func (em *ExpireMap) GetBool(key string) (v bool, ok bool) {
v, ok = em.data[key].(bool)
return
}
-// За определен ключ тази функция трябва да върне времето, когато той ще изтече. Втората върната стойност е true когато ключа е намерен в хранилището. Когато не е намерен функцията трябва да върне нулевото време и false.
+// За определен ключ тази функция трябва да върне времето, когато той ще изтече. Втората върната стойност е true когато ключът е намерен в хранилището. Когато не е намерен функцията трябва да върне нулевото време и false.
func (em *ExpireMap) Expires(key string) (v time.Time, ok bool) {
v, ok = em.expires[key]
return
}
-// Премахва ключа от хранилището. Когато няма такъв ключ метода не трябав да гърми.
+// Requires lock
+func (em *ExpireMap) delete(key string) {
+ delete(em.data, key)
+ delete(em.expires, key)
+ delete(em.stopChannels, key)
+}
+
+// Премахва ключа от хранилището. Когато няма такъв ключ методът не трябва да гърми.
func (em *ExpireMap) Delete(key string) {
em.lock()
defer em.unlock()
- delete(em.data, key)
- delete(em.expires, key)
+ em.stopGoroutine(key)
+ em.delete(key)
}
// Казва дали ключ е в хранилището или не. Отново, ключове са в хранилището само ако техния expire не е вече изтекъл.
func (em *ExpireMap) Contains(key string) (ok bool) {
_, ok = em.Get(key)
return
}
// Връща големината на хранилището. Големина е бройката неизтекли ключове, които са в него.
func (em *ExpireMap) Size() int {
return len(em.data)
}
func (em *ExpireMap) addValueToIntOrString(key string, delta int) error {
em.lock()
defer em.unlock()
- if !em.Contains(key) {
- return nil
- }
if v, ok := em.GetInt(key); ok {
em.data[key] = v + delta
} else if v, ok := em.GetString(key); ok {
intV, ok := strconv.Atoi(v)
if ok != nil {
return ok
}
em.data[key] = strconv.Itoa(intV + delta)
} else {
- return &ExpireMapError{""}
+ return &ExpireMapError{"Could not find int or string value at " + key}
}
return nil
}
// За ключ увеличава стойността му числово с 1 когато тази стойност е от тип int или стринг, представящ цяло число. Когато стойността е от тип, различен от тези неща функцията трябва да върне грешка, различна от nil. float32 и float64 не са целочислен типове. Времето на изтичане на ключа не трябва да се променя.
func (em *ExpireMap) Increment(key string) error {
return em.addValueToIntOrString(key, 1)
}
// Същото като Increment, но намалява стойността с 1 вместо да я увеличава.
func (em *ExpireMap) Decrement(key string) error {
return em.addValueToIntOrString(key, -1)
}
-// Ако стойността за ключа key е от тип string, то всички букви в този string трябва да станат главни. При успех метода трябва да върне nil, а при невъзможност да изпълни задачата - грешка.
+// Ако стойността за ключа key е от тип string, то всички букви в този string трябва да станат главни. При успех методът трябва да върне nil, а при невъзможност да изпълни задачата - грешка.
func (em *ExpireMap) ToUpper(key string) (ok error) {
em.lock()
defer em.unlock()
if v, ok := em.GetString(key); ok {
em.data[key] = strings.ToUpper(v)
}
return
}
func (em *ExpireMap) ToLower(key string) (ok error) {
em.lock()
defer em.unlock()
if v, ok := em.GetString(key); ok {
em.data[key] = strings.ToLower(v)
}
return
}
// Тази функция трябва да върне канал, по който да идват ключовете, които са изтекли. Те трябва да се пускат по канала в момента на изтичането си. Ключове, които са изтрити с Delete или Cleanup не трябва да се връщат по този канал. Единствено тези, които са достигнали до времето си на изтичане. Не е задължително през цялото време някой да чете от върнатия канал. Ключове добавени в инстанцията след извикването на ExpireChan също трябва да се пратят по канала, когато тяхното време дойде.
func (em *ExpireMap) ExpiredChan() <-chan string {
- return em.channel
+ return em.expiredChannel
}
// Изчиства хранилището. След извикване на функцията в ExpireMap-a не трябва да се съдържа нито един ключ.
func (em *ExpireMap) Cleanup() {
em.lock()
defer em.unlock()
- for k := range em.data {
- delete(em.data, k)
+ for key := range em.data {
+ em.stopGoroutine(key)
+ em.delete(key)
}
- for k := range em.expires {
- delete(em.expires, k)
- }
}
// Трябва да освободи всички ресурси, които е използвал този ExpireMap. Това означава изчистване на всички структури, затваряне на всички канали и спиране на всички горутини, свързани с нормалната му работа. След извикване на Destroy тази инстанция на ExpireMap няма да бъде повече използвана. Този метод би трябвало да се ползва по подобен начин:
func (em *ExpireMap) Destroy() {
em.lock()
+ for key := range em.data {
+ em.stopGoroutine(key)
+ }
em.data = nil
em.expires = nil
- close(em.channel)
- close(em.m)
- // TODO: close goroutines
+ em.stopChannels = nil
+ close(em.expiredChannel)
+ close(em.mutex)
}

Има още някакви неща които ми хрумнаха, но не съм имплементирал - не знам дали има смисъл.

  1. В момента за спиране на горутина на момента използвам небуфериран канал. Като цяло това е доста просто решение, но не знам дали няма да е по-добре да го сменя с някакъв WaitGroup (особено при Destroy реално в момента имам на практика синхронен код).

  2. Като цяло понеже виждам, че и в тестовете на домашното се използват повече ядра и процеси - си мислех, че бих могъл методите lock и unlock да приемат аргумент key string и съответно да не се заключва всичко, а само някакво подмножество - тоест примерно на всеки ключ мога да съпоставям число от 0 до n чрез някаква хешираща функция и така да имам n + 1 lock-a вместо един - сигурно ще се забързат нещата. Тъй като това обаче е някакъв вид оптимизация не знам дали има нужда да се прави са решението.

  3. В случая съм оставил описанията на функциите преди тях - това е удобно при писането на имплементацията, а и после като документация и сверяване дали отговарят на всички условия. Тъй като са на български обаче поне на мен ми е малко странно - по-добре ли ще е ако ги махна?

Сега иначе забелязах, че при голямо натоварване на ExpireMap-а май се получава, че има шанс четенето от канала да пропусне някоя стойност, понеже буфера не е достатъчно голям, а имам default при писането. Тоест ако има голямо натоварване може да се получи да презапиша стойността преди някой още да я е прочел. За да бъде правилно поведението обаче май трябва когато някой поиска expiredChan да ги пазя и да ги пускам в канала, а това не звучи добре.

По-добре ли ще да променя вместо default на timeout от 10 ms? Така ако приемем, че в спецификацията на пише, че трябва да се върне резултат по канала в близките 10ms, ако никой не го прочете в този период да не се гарантира, че ще е налична стойността. Така ще има все пак 10ms в които да може да се прочете.