Skip to content

Instantly share code, notes, and snippets.

@artuross
Created December 16, 2022 12:31
Show Gist options
  • Save artuross/ffdde0fa10f57282ef28f699991513c4 to your computer and use it in GitHub Desktop.
Save artuross/ffdde0fa10f57282ef28f699991513c4 to your computer and use it in GitHub Desktop.
Cybozu
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