// Copyright 2017-2018, Square, Inc.

// Package job provides job-related interfaces, data structures, and errors.
// To avoid an import cycle, this package must not have external dependencies
// because everything else depends on it.
package job

// A Job is the smallest, reusable building block in Spin Cycle that has meaning
// by itself. A job should do one thing and be reusable. For example, job type
// "net/down-ip" removes an IP address from a network interface. This job is
// meaningful by itself and reusable in different requests.
//
// All jobs are user-defined and instantiated by the external job factory (EJF),
// which is also user-defined (job/external.JobFactory). Spin Cycle only defines
// the Job interface; jobs are written by the user. A job must implement the Job
// interface and be able to accomplish its purpose only through this interface.
// Spin Cycle is completely unaware of job implementation details.
//
// The Job interface has two sides: one for the Request Manager (RM), the other
// for the Job Runner (JR). The RM calls Create and Serialize, and the JR calls
// the other methods. The call sequence is: Create, Serialize, Deserialize, Run.
type Job interface {
	// Create allows the job to get and save internal data needed to run later.
	// The job can save jobArgs and set new ones for other jobs.
	//
	// This method is only called once by the Request Manager when constructing
	// a job chain. Construction of the job chain fails if an error is returned.
	Create(jobArgs map[string]interface{}) error

	// Serialize returns all internal data needed to run later. The reciprocal
	// method is Deserialize.
	//
	// This method is only called once by the Request Manager when constructing
	// a job chain. Construction of the job chain fails if an error is returned.
	Serialize() ([]byte, error)

	// Deserialize sets internal data needed to run later. The reciprocal
	// method is Serialize.
	//
	// This method is only called once by the Job Runner when reconstructing
	// a job chain. Reconstruction of the job chain fails if an error is
	// returned.
	Deserialize([]byte) error

	// Run runs the job using its interal data and the run-time jobData from
	// previously-ran (upstream) jobs. Run can modify jobData. Run is expected
	// to block, but the job must respond to Stop and Status while running.
	// The returned error, if any, indicates a problem before or after running
	// the job. The final state of the job is returned in the Return structure,
	// along with other things like the error and exit code (if there was one).
	// The final state is the most important field because it is used by the JR
	// to determine how to handle a job when it's done running.
	//
	// Currently, the Job Runner only calls this method once. Resuming a job is
	// not currently supported.
	Run(jobData map[string]interface{}) (Return, error)

	// Stop stops a job. The Job Runner calls this method when stopping a job
	// chain before it has completed. The job must respond to Stop while Run
	// is executing. Stop is expected to block but also return quickly.
	Stop() error

	// Status returns the real-time status of the job. The job must respond
	// to Status while Run is executing. Status is expected to block but also
	// return quickly.
	Status() string

	// Id returns the Id structure the job was created with. This is used primarily
	// for status and logging by Spin Cycle. The Id values for the job must exactly
	// match the Id values passed to the external job factory which created the job,
	// else Spin Cycle returns ErrWrongJobId.
	Id() Id
}

// Id represents how jobs are uniquely identified per request. Type and Name are
// user-defined in the external job factory (EJF) and request spec, respectively.
// Id is defined per request by Spin Cycle. An example for each value:
//
//   Type:  net/down-ip
//   Name:  down-sip-on-eth0
//   Id:    9m4e2mr0
//
// Job types are defined in code and in the external job factory (EJF). They
// represent what the job is and does. Job names are defined in request specs;
// they are named instantiantions of job types. The name should be short but
// meaningful in the context of the request. Job IDs are short, random strings
// generated by Spin Cycle. Id is only guaranteed to be unique per request, not
// globally.
type Id struct {
	// User-defined low-level job type implemented in code and made by external
	// job factory (EJF). Any value that the EJF is coded to make is valid. Job
	// types are heavily reused by being given unique names in request specs.
	Type string

	// User-defined job name in a request spec. Names must be unique per sequence.
	// During sequence expansion, job names remain the same and become uniquely
	// identified from one another by job ID.
	Name string

	// Spin Cycle-defined job ID unique per request. Job IDs distinguish named
	// jobs in expanded sequences.
	Id string

	// RequestId of the request that created the job. This is only informational
	// for reporting/loggging/tracing.
	RequestId string
}

// NewId is a convenience function for creating a new Id with the given values.
// It's used by job-runner/runner/Factory.Make to create an Id from a proto.Job.
func NewId(jobType, jobName, jobId string) Id {
	return Id{
		Type: jobType,
		Name: jobName,
		Id:   jobId,
	}
}

func NewIdWithRequestId(jobType, jobName, jobId, reqId string) Id {
	return Id{
		Type:      jobType,
		Name:      jobName,
		Id:        jobId,
		RequestId: reqId,
	}
}

// A Factory instantiates a Job of the given type. A factory only instantiates
// a new Job object, it must not call any Job interface methods on the newly
// created job. If an error is returned, the returned Job should be ignored.
//
// Spin Cycle does not and should not know how to instantiate jobs because they
// are external (imported in external/factory.go). Once a job is instantiated with
// external, implementation-specific details, Spin Cycle only needs to know and
// use the Job interface methods.
//
// Spin Cycle calls the job's Id method to ensure that it returns the same Id
// values that it was created with. If not, ErrWrongJobId is returned.
type Factory interface {
	Make(id Id) (Job, error)
}

// Return represents return values and output from a job. State indicates how
// the job completed. If State == proto.STATE_COMPLETE, the job completed
// successfully. Anything else indicates that the job failed or didn't complete,
// which will probably cause the job chain to stop. The job is responsible for
// adhering to this convention.
//
// State, Exit, and Error are not mutually exclusive. A job can return State =
// STATE_COMPLETE to indicate success but also a non-nil Error or a non-zero
// Exit. This is useful for idempotent jobs and logging that the job was
// successful because it handled being re-ran. For example, a job could delete
// a record, but when re-ran the record has already been deleted, so the job
// is successful but reports Error = ErrRecordNotFound for logging.
type Return struct {
	State  byte   // proto/STATE_ const
	Exit   int64  // Unix exit code
	Error  error  // Go error
	Stdout string // stdout output
	Stderr string // stderr output
}
