Created
December 16, 2022 12:31
-
-
Save artuross/ffdde0fa10f57282ef28f699991513c4 to your computer and use it in GitHub Desktop.
Cybozu
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package solution | |
import ( | |
"fmt" | |
"sync" | |
"time" | |
) | |
// I'm not sure if I understand the problem correctly. | |
// My understanding is that we should avoid making concurrent calls to the same URL. | |
// In other words, if we have 3 Fetch calls with the same URL, | |
// we should only make one HTTP request and send the response to all 3 channels. | |
// However, if we have Fetch call to a URL, it finishes and then we make another call | |
// to the same URL, new HTTP request should be made (again preventing concurrent calls). | |
// | |
// Because I'm not sure of this, and the original code had `cache`, I've also added this | |
// functionality, but it must be enabled by setting NewResponseCache in NewDownloader function. | |
// With cache enabled, the URL will be downloaded only once and the response will | |
// be stored for later reuse. | |
// Typically this would be in multiple files/packages. | |
// ------------------------------------------------------------------------------------------ | |
// DEBUGGER | |
// Debugger is a simple interface for debugging. | |
type Debugger interface { | |
Debugln(values ...interface{}) | |
} | |
// Debug is a simple implementation of Debugger. | |
type Debug struct{} | |
func NewDebug() *Debug { | |
return &Debug{} | |
} | |
func (d *Debug) Debugln(values ...interface{}) { | |
fmt.Println(values...) | |
} | |
// NoopDebug is a debugger that does nothing. | |
type NoopDebug struct{} | |
func NewNoopDebug() *NoopDebug { | |
return &NoopDebug{} | |
} | |
func (d *NoopDebug) Debugln(values ...interface{}) {} | |
// ------------------------------------------------------------------------------------------ | |
// CACHE | |
// Cache is an interface for a cache that stores the downloaded content for each URL. | |
type Cache interface { | |
Get(url string) ([]byte, bool) | |
Set(url string, resp []byte) | |
} | |
// ResponseCache is a simple cache that stores the downloaded content for each URL. | |
type ResponseCache struct { | |
mu sync.RWMutex | |
// cache stores the downloaded content for each URL for later reuse | |
cache map[string][]byte | |
} | |
// NewResponseCache creates a new ResponseCache. | |
func NewResponseCache() *ResponseCache { | |
return &ResponseCache{ | |
cache: make(map[string][]byte), | |
} | |
} | |
// Get returns the response for the given URL. | |
func (c *ResponseCache) Get(url string) ([]byte, bool) { | |
c.mu.RLock() | |
defer c.mu.RUnlock() | |
resp, ok := c.cache[url] | |
return resp, ok | |
} | |
// Set stores the response for the given URL. | |
func (c *ResponseCache) Set(url string, resp []byte) { | |
c.mu.Lock() | |
defer c.mu.Unlock() | |
c.cache[url] = resp | |
} | |
// NoopCache is a cache that does nothing. | |
type NoopCache struct{} | |
func NewNoopCache() *NoopCache { | |
return &NoopCache{} | |
} | |
func (c *NoopCache) Get(url string) ([]byte, bool) { | |
return nil, false | |
} | |
func (c *NoopCache) Set(url string, resp []byte) {} | |
// ------------------------------------------------------------------------------------------ | |
// DOWNLOADER | |
// Downloader downloads the content of a URL, preventing concurrent calls to the same URL. | |
type Downloader struct { | |
// mu protects the inflight map. | |
mu sync.RWMutex | |
// inflight stores a map of URLs that are currently being downloaded. | |
// The value is a channel that will be closed when the download is complete. | |
inflight map[string][]chan<- []byte | |
// logger to use for debugging | |
debugger Debugger | |
// cache implementation to use for caching | |
cache Cache | |
} | |
// NewDownloader creates a new Downloader. | |
func NewDownloader(debugger Debugger, cache Cache) *Downloader { | |
return &Downloader{ | |
mu: sync.RWMutex{}, | |
inflight: make(map[string][]chan<- []byte), | |
debugger: debugger, | |
cache: cache, | |
} | |
} | |
// Fetch downloads the content of the given URL, preventing concurrent calls to the same URL. | |
func (d *Downloader) Fetch(url string) []byte { | |
resp, has := d.cache.Get(url) | |
if has { | |
return resp | |
} | |
// respChan is where the response will be sent to | |
// it's buffered as we don't want to block the sender | |
respChan := make(chan []byte, 1) | |
// check if the URL is already being downloaded | |
// mutex is not unlocked right after | |
// so it's important to keep track of it | |
d.mu.Lock() | |
_, hasInflightRequests := d.inflight[url] | |
// if it is, add the response channel to the map | |
// and wait for the response | |
if !hasInflightRequests { | |
d.inflight[url] = make([]chan<- []byte, 0, 1) | |
} | |
// add the response channel to the map | |
d.inflight[url] = append(d.inflight[url], respChan) | |
// unlock the mutex | |
d.mu.Unlock() | |
// download the URL and send response to all subscribed channels | |
if !hasInflightRequests { | |
d.fetch(url) | |
} | |
// technically, we can just return resp here | |
return d.waitForResponse(respChan) | |
} | |
// fetch is a helper function that downloads the URL and sends the response to all | |
// subscribed channels. If cache is enabled, the response is stored in the cache. | |
func (d *Downloader) fetch(url string) []byte { | |
d.debugger.Debugln("Downloading URL: ", url) | |
// download the URL | |
resp := FakeHTTPGet(url) | |
d.mu.Lock() | |
defer d.mu.Unlock() | |
// get the channels | |
// and clean map entry | |
respChannels, ok := d.inflight[url] | |
if ok { | |
delete(d.inflight, url) | |
} | |
d.cache.Set(url, resp) | |
// send responses | |
d.sendResponses(url, resp, respChannels) | |
return resp | |
} | |
// sendResponses sends the response to all subscribed channels. | |
func (d *Downloader) sendResponses(url string, resp []byte, respChannels []chan<- []byte) { | |
for _, respChan := range respChannels { | |
d.debugger.Debugln("Sending response to: ", url) | |
respChan <- resp | |
} | |
} | |
// waitForResponse waits for the response to be sent to the channel. The channel is | |
// closed once the message is received. | |
func (d *Downloader) waitForResponse(respChan chan []byte) []byte { | |
// wait for the response | |
resp := <-respChan | |
// we can close the channel as we don't need it anymore | |
close(respChan) | |
// return the response | |
return resp | |
} | |
// Do not touch the code below. | |
func FakeHTTPGet(u string) []byte { | |
time.Sleep(10 * time.Millisecond) | |
return []byte(u) | |
} | |
var downloader = NewDownloader(NewNoopDebug(), NewNoopCache()) | |
func Solution(A []string) bool { | |
wg := new(sync.WaitGroup) | |
results := make(map[string][]byte) | |
var mu sync.Mutex | |
for _, u := range A { | |
wg.Add(1) | |
go func(u string) { | |
defer wg.Done() | |
content := downloader.Fetch(u) | |
mu.Lock() | |
results[u] = content | |
mu.Unlock() | |
}(u) | |
} | |
wg.Wait() | |
return len(A) > 2 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment