Skip to content
This repository was archived by the owner on Dec 5, 2024. It is now read-only.

Commit 6bf6236

Browse files
committed
Initial commit
0 parents  commit 6bf6236

File tree

5 files changed

+296
-0
lines changed

5 files changed

+296
-0
lines changed

Diff for: README.md

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Ingram Micro Senior Backend Developer test
2+
3+
This task challenges you to implement in Go an in-memory queue satisfying a specific interface. Our intention behind this task is to have something to talk about in an interview, so it does not matter if you get stuck and cannot complete it: just deliver whatever you got.
4+
5+
## Setup
6+
7+
- [Install Go](https://golang.org/doc/install#install) if you do not have it already
8+
- Clone this repository outside your GOPATH (because it is using [go modules](https://blog.golang.org/using-go-modules))
9+
10+
## Develop
11+
12+
You should write your code in the queue package, modifying the worker.go file to complete the New method and maybe in other .go files. You should probably leave the interfaces.go file as it is.
13+
14+
As specified below, the delivery format is a git repository. It would be nice that the git history showed your progress.
15+
16+
### Run your code
17+
18+
The main.go file in the top-level directory of this repo will use your implementation to approximate pi. Before delivering your task you should try your best to make it work. You can run it with:
19+
20+
```Bash
21+
go run main.go
22+
```
23+
24+
### Tips
25+
26+
* Channels are not a silver bullet in go
27+
* You will probably need to get familiar with the [context package](https://golang.org/pkg/context/). The following blog post can be an introductory read: http://p.agnihotry.com/post/understanding_the_context_package_in_golang/
28+
29+
### Bonus tracks
30+
31+
If you really want to impress us by going the extra mile you could do one of the following:
32+
33+
- Add tests for your code
34+
- Dockerize the running of the main.go file
35+
- Implement a persisted (file, redis or something else -based) implementation of the queue, maybe in another package, that satisfies the interface.
36+
37+
## Deliver
38+
39+
Create a public github repository and push your code there. Then send us an email with a link to it.

Diff for: go.mod

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/ingrammicro/backend-test
2+
3+
go 1.12

Diff for: main.go

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"log"
9+
"math/big"
10+
"math/rand"
11+
"time"
12+
13+
"github.com/ingrammicro/backend-test/queue"
14+
)
15+
16+
// piComputeData holds both the input and output of a pi processing job
17+
// Total (both input and output) is the number of random points to pick
18+
// in a [0,1)x[0,1) square.
19+
// InCircle (only output) is the number of the randomly picked points that
20+
// where inside the circle of radius 1 cented in (0,0).
21+
type piComputeData struct {
22+
InCircle uint64 `json:"i"`
23+
Total uint64 `json:"t"`
24+
}
25+
26+
// piProcessor is a processor that can work out pi processing jobs
27+
type piProcessor struct{}
28+
29+
// Process processes a pi processing job. To do so, it extracts piComputeData from
30+
// the given job, computes it and stores it back into the job. It returns an error
31+
// if any of the three operations fail.
32+
func (pp piProcessor) Process(ctx context.Context, j queue.JobProcessingAccess) error {
33+
pcd := &piComputeData{}
34+
err := j.GetData(pcd)
35+
if err != nil {
36+
return err
37+
}
38+
err = pcd.Compute(ctx)
39+
if err != nil {
40+
return err
41+
}
42+
err = j.SetData(ctx, pcd)
43+
if err != nil {
44+
return err
45+
}
46+
return nil
47+
}
48+
49+
// Compute picks a Total number of points in the [0,1)x[0,1) square
50+
// and checks for each of one if they are inside the circle of radius 1 cented in (0,0).
51+
// Specifically, given a (x,y) point, it checks whether x²+y² <= 1.
52+
// It updates InCircle with the number of points that were inside.
53+
func (pcd *piComputeData) Compute(ctx context.Context) error {
54+
r := rand.New(rand.NewSource(time.Now().UTC().UnixNano()))
55+
for i := uint64(0); i < pcd.Total; i++ {
56+
x, y := r.Float64(), r.Float64()
57+
if (x*x)+(y*y) <= 1 {
58+
pcd.InCircle++
59+
}
60+
}
61+
return nil
62+
}
63+
64+
func (pcd *piComputeData) String() string {
65+
return fmt.Sprintf("%d/%d", pcd.InCircle, pcd.Total)
66+
}
67+
68+
// Marshal encodes the piComputeData into the returned byte slice
69+
// as JSON.
70+
func (pcd *piComputeData) Marshal() ([]byte, error) {
71+
buf := &bytes.Buffer{}
72+
err := json.NewEncoder(buf).Encode(pcd)
73+
if err != nil {
74+
return nil, err
75+
}
76+
return buf.Bytes(), nil
77+
}
78+
79+
// Unmarshal decodes the JSON in the given byte slice
80+
// and fills the piComputeData with it.
81+
func (pcd *piComputeData) Unmarshal(b []byte) error {
82+
return json.NewDecoder(bytes.NewReader(b)).Decode(pcd)
83+
}
84+
85+
// main pushes numberOfJobs pi processing jobs (each computing a million points),
86+
// starts 10 workers, waits for all the jobs to be processed and then aggregates
87+
// the results of the jobs to approximate pi. Finally, it prints the approximation
88+
// and exits orderly.
89+
func main() {
90+
const numberOfJobs = 10000
91+
ctx, cancelCtx := context.WithCancel(context.Background())
92+
defer cancelCtx()
93+
client, worker := queue.New(piProcessor{})
94+
log.Printf("Pushing %d pi processing jobs...", numberOfJobs)
95+
for i := 0; i < numberOfJobs; i++ {
96+
client.CreateJob(ctx, fmt.Sprintf("j-%d", i), &piComputeData{Total: 1000000})
97+
}
98+
log.Print("Starting 10 workers...")
99+
workerStopped := make(chan struct{})
100+
go func() {
101+
worker.Run(ctx, 10)
102+
close(workerStopped)
103+
}()
104+
log.Print("Waiting for results and aggregating them...")
105+
result := &big.Rat{}
106+
for i := 0; i < numberOfJobs; i++ {
107+
jobID := fmt.Sprintf("j-%d", i)
108+
for {
109+
job, err := client.GetJob(ctx, jobID)
110+
if err != nil {
111+
log.Fatal(err)
112+
}
113+
if job == nil {
114+
log.Fatalf("Job %q could not be found", jobID)
115+
}
116+
state := job.State()
117+
if state == queue.Failed {
118+
log.Fatal(job.Error())
119+
}
120+
if state == queue.Finished {
121+
var partialResult piComputeData
122+
err = job.GetData(&partialResult)
123+
if err != nil {
124+
log.Fatal(err)
125+
}
126+
result = result.Add(result, big.NewRat(4*int64(partialResult.InCircle), int64(partialResult.Total)))
127+
break
128+
}
129+
time.Sleep(5 * time.Second) // Wait a bit for the job to finish
130+
}
131+
}
132+
cancelCtx()
133+
result = result.Mul(result, big.NewRat(1, numberOfJobs))
134+
log.Printf("Result is %+v = %s", result, result.FloatString(20))
135+
log.Printf("Preparing to exit...")
136+
<-workerStopped
137+
log.Printf("Exiting")
138+
}

Diff for: queue/interface.go

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package queue
2+
3+
import (
4+
"context"
5+
)
6+
7+
// State represents the state of a job
8+
type State string
9+
10+
// MarshalUnmarshaler wraps the Marshal and Unmarshal methods.
11+
// Job payloads must implement this interface that allows
12+
// converting them into a slice of bytes and back.
13+
//
14+
// The Marshal method marshals the object into a stream of bytes
15+
// and returns the slice of bytes of an error.
16+
//
17+
// The Unmarshal method unmarshals an object from a stream of bytes
18+
// into itself and may return an error.
19+
//
20+
// Implementations should ensure the unmarshaling of the slice of bytes
21+
// provided by the marshaling of an object provides essentially the same
22+
// object.
23+
//
24+
// This interface has already an implementation by us in the main.go file.
25+
type MarshalUnmarshaler interface {
26+
Marshal() ([]byte, error)
27+
Unmarshal([]byte) error
28+
}
29+
30+
// Job is the interface that wraps the methods
31+
// use to read data from a job obtained from a client.
32+
//
33+
// The ID methods provide the ID of the job.
34+
//
35+
// The GetData method unmarshals into data the payload of the job.
36+
//
37+
// The State method returns the State of the job.
38+
//
39+
// The Error method returns a string describing the error with which a job failed.
40+
type Job interface {
41+
ID() string
42+
GetData(data MarshalUnmarshaler) error
43+
State() State
44+
Error() string
45+
}
46+
47+
// JobProcessingAccess is just the Job interface with an extra method
48+
// that allows setting the data payload of the job. It is meant to be
49+
// used by job processors launched by workers, which will need to store
50+
// their results there.
51+
//
52+
// The SetData method takes some data and updates the
53+
// job's payload data with it. Implementations should use the context
54+
// argument to allow canceling or expiration of the SetData operation,
55+
// and return an error in that case or if the payload data update cannot
56+
// be performed.
57+
type JobProcessingAccess interface {
58+
Job
59+
SetData(ctx context.Context, data MarshalUnmarshaler) error
60+
}
61+
62+
// A Processor defines the worker's job execution.
63+
// It returns an error:
64+
// * If the error is not nil, the job is marked as Failed.
65+
// * If the error is nil the job is marked as finished
66+
// (successfully).
67+
//
68+
// This interface has already an implementation by us in the main.go file.
69+
type Processor interface {
70+
Process(ctx context.Context, j JobProcessingAccess) error
71+
}
72+
73+
// Worker is an interface that wraps the Run method, which
74+
// allows processing jobs in a queue.
75+
//
76+
// Implementations should attempt to process as many jobs as
77+
// the given worker integer simultaneously using a Processor.
78+
// They should also use the given context to allow users to timeout
79+
// or cancel processing, returning only after all workers have stopped.
80+
type Worker interface {
81+
Run(ctx context.Context, workers int) error
82+
}
83+
84+
// Client is an interface that allows pushing jobs into a queue
85+
// and querying their state and results.
86+
//
87+
// Implementations of GetJob should return
88+
// * a nil job and a nil error when the job is not found
89+
// * the job and a nil error when the job is found
90+
// * a nil job and an error, when some error prevents the retrieval
91+
// of the job
92+
type Client interface {
93+
CreateJob(ctx context.Context, id string, initialData MarshalUnmarshaler) error
94+
GetJob(ctx context.Context, id string) (Job, error)
95+
}
96+
97+
const (
98+
// Queued but not processing yet
99+
Queued State = "queued"
100+
// Processing - The worker got active
101+
Processing State = "processing"
102+
// Failed by worker or from outside (timeout) - See Jobs's Error field
103+
Failed State = "failed"
104+
// Finished successfully
105+
Finished State = "finished"
106+
)

Diff for: queue/worker.go

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package queue
2+
3+
// New takes a processor and returns both
4+
// a client and a worker. The client allows
5+
// pushing jobs to the queue (with CreateJob)
6+
// and the worker can run those jobs using
7+
// the given Processor
8+
func New(p Processor) (Client, Worker) {
9+
return nil, nil
10+
}

0 commit comments

Comments
 (0)