Repository: armadaproject/armada
Pull request #2085 (2023-02-04 19:19:38)

Api To Direct Jobs To Pulsar Scheduler
Changes to allow Legacy (Redis) and New (Pulsar) schedulers to run in parallel.  Basic strategy is as follows:

* Pulsar messages are now tagged with an attribute, `schedulerName` that indicates whether the message is for the legacy scheduler, the new scheduler or both. Compoenents folter out messages they are not interested in (e.g. the scheduler ingester doesn't care about events for the legacy scheduler so can just ignore them).

* The Submit API (`submit_to_log.go`) has been updated so that on submission, a job is allocated to either the new scheduler or the old scheduler.  This is done by first checking if the job is schedulable on either scheduler.  If the job is schedulable only on one of the schedulers, it is assigned to that one, if it is schedulable on both then it is assigned to the puslar scheduler at a probability defined in config (default is 0%).

* For cancelling and reprioritising  jobs we have various code paths that require the queue and jobset to be retrieved from redis.  For Jobs assigned to the Legacy scheduler this is not an issue, because all jobs are stored in redis and therefore can be retived by the api when the cancel request is retrieved.  For jobs assigned to the Pulsar scheduler, this is an issue as the info was not previously in redis and We do not want the api hitting the scheduler (or it's db)  to retrieve this.  For now I've solved this by storing a minimal Job in Redis which the api can retrieve.

* Both the legacy scheduler and the pulsar scheduler store `schedulerobjects.Executor`s in redis when they get lease calls.  By  storing this in redis this means we can reconstruct the nodedb for both legacy and pulsar scheduler in the api and therefore validate that Jobs can fit on any node at submission time.  Note that this means I have *disabled the old "does the job fit"* code path and now we only use Albin's new code path that utilises JobDb.  The other benefit of storing this data in Redis is that it means that both the Pulsar scheduler and the Legacy Scheduler can see each other's utilisation and thus calculate fair share properly.  Note that I haven't actually added the code to do this yet though.

Note that the use of redis for storing the data above (minimal Pulsar jobs + executor info) is hopefully temporary as for as long as we do this we cannot remove the redis dependency from the api.  I think we can takle this after we have switrched schedulers, however.


codecov-commenter: # [Codecov](https://codecov.io/gh/armadaproject/armada/pull/2085?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject) Report
Base: **47.11**% // Head: **47.16**% // Increases project coverage by **`+0.04%`** :tada:
> Coverage data is based on head [(`3f3c7bb`)](https://codecov.io/gh/armadaproject/armada/pull/2085?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject) compared to base [(`8116fa6`)](https://codecov.io/gh/armadaproject/armada/commit/8116fa6fd7ce525cd56eb18bbc57c0b463cfd611?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject).
> Patch coverage: 22.81% of modified lines in pull request are covered.

:mega: This organization is not using Codecov’s [GitHub App Integration](https://github.com/apps/codecov). We recommend you install it so Codecov can continue to function properly for your repositories. [Learn more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject)

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #2085      +/-   ##
==========================================
+ Coverage   47.11%   47.16%   +0.04%     
==========================================
  Files         229      230       +1     
  Lines       30251    30641     +390     
==========================================
+ Hits        14252    14451     +199     
- Misses      14949    15120     +171     
- Partials     1050     1070      +20     
```

| Flag | Coverage Δ | |
|---|---|---|
| airflow-operator | `89.13% <ø> (ø)` | |
| armada-server | `46.17% <22.81%> (+0.06%)` | :arrow_up: |
| python-client | `93.83% <ø> (ø)` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/armadaproject/armada/pull/2085?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject) | Coverage Δ | |
|---|---|---|
| [internal/armada/repository/event\_store.go](https://codecov.io/gh/armadaproject/armada/pull/2085?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#diff-aW50ZXJuYWwvYXJtYWRhL3JlcG9zaXRvcnkvZXZlbnRfc3RvcmUuZ28=) | `0.00% <0.00%> (ø)` | |
| [internal/armada/server/reporting.go](https://codecov.io/gh/armadaproject/armada/pull/2085?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#diff-aW50ZXJuYWwvYXJtYWRhL3NlcnZlci9yZXBvcnRpbmcuZ28=) | `63.87% <ø> (+3.45%)` | :arrow_up: |
| [internal/armada/server/submit\_from\_log.go](https://codecov.io/gh/armadaproject/armada/pull/2085?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#diff-aW50ZXJuYWwvYXJtYWRhL3NlcnZlci9zdWJtaXRfZnJvbV9sb2cuZ28=) | `4.68% <0.00%> (ø)` | |
| [internal/armada/server/submit\_to\_log.go](https://codecov.io/gh/armadaproject/armada/pull/2085?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#diff-aW50ZXJuYWwvYXJtYWRhL3NlcnZlci9zdWJtaXRfdG9fbG9nLmdv) | `0.00% <0.00%> (ø)` | |
| [internal/scheduler/nodedb.go](https://codecov.io/gh/armadaproject/armada/pull/2085?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#diff-aW50ZXJuYWwvc2NoZWR1bGVyL25vZGVkYi5nbw==) | `68.46% <ø> (+4.12%)` | :arrow_up: |
| [internal/scheduler/schedulerapp.go](https://codecov.io/gh/armadaproject/armada/pull/2085?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#diff-aW50ZXJuYWwvc2NoZWR1bGVyL3NjaGVkdWxlcmFwcC5nbw==) | `0.00% <0.00%> (ø)` | |
| [internal/scheduleringester/ingester.go](https://codecov.io/gh/armadaproject/armada/pull/2085?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#diff-aW50ZXJuYWwvc2NoZWR1bGVyaW5nZXN0ZXIvaW5nZXN0ZXIuZ28=) | `0.00% <0.00%> (ø)` | |
| [.../scheduler/schedulerobjects/schedulerobjects.pb.go](https://codecov.io/gh/armadaproject/armada/pull/2085?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#diff-aW50ZXJuYWwvc2NoZWR1bGVyL3NjaGVkdWxlcm9iamVjdHMvc2NoZWR1bGVyb2JqZWN0cy5wYi5nbw==) | `1.15% <0.43%> (-0.05%)` | :arrow_down: |
| [internal/armada/server/lease.go](https://codecov.io/gh/armadaproject/armada/pull/2085?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#diff-aW50ZXJuYWwvYXJtYWRhL3NlcnZlci9sZWFzZS5nbw==) | `10.82% <7.69%> (+0.02%)` | :arrow_up: |
| [internal/armada/repository/job.go](https://codecov.io/gh/armadaproject/armada/pull/2085?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#diff-aW50ZXJuYWwvYXJtYWRhL3JlcG9zaXRvcnkvam9iLmdv) | `65.10% <43.58%> (-1.23%)` | :arrow_down: |
| ... and [15 more](https://codecov.io/gh/armadaproject/armada/pull/2085?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject) | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/armadaproject/armada/pull/2085?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject).


Patch:
diff --git a/cmd/eventsprinter/logic/logic.go b/cmd/eventsprinter/logic/logic.go
index 9bf8b702..36c16064 100644
--- a/cmd/eventsprinter/logic/logic.go
+++ b/cmd/eventsprinter/logic/logic.go
@@ -33,11 +33,6 @@ func PrintEvents(url, topic, subscription string, verbose bool) error {
 			}
 			consumer.Ack(msg)
 
-			// We're only interested in control messages.
-			if !armadaevents.IsControlMessage(msg) {
-				continue
-			}
-
 			sequence := &armadaevents.EventSequence{}
 			err = proto.Unmarshal(msg.Payload(), sequence)
 			if err != nil {
diff --git a/config/armada/config.yaml b/config/armada/config.yaml
index 0dba2d39..e3311f91 100644
--- a/config/armada/config.yaml
+++ b/config/armada/config.yaml
@@ -6,6 +6,9 @@ corsAllowedOrigins:
   - http://localhost:8089
   - http://localhost:10000
 cancelJobsBatchSize: 1000
+pulsarSchedulerEnabled: false
+probabilityOfUsingPulsarScheduler: 0
+ignoreJobSubmitChecks: true
 grpc:
   keepaliveParams:
     maxConnectionIdle: 5m
diff --git a/go.mod b/go.mod
index e1b6cf20..a2597358 100644
--- a/go.mod
+++ b/go.mod
@@ -95,6 +95,7 @@ require (
 	github.com/magefile/mage v1.14.0
 	github.com/matryer/moq v0.3.0
 	github.com/openconfig/goyang v1.2.0
+	github.com/pingcap/log v0.0.0-20210906054005-afc726e70354
 	github.com/prometheus/common v0.37.0
 	github.com/sanity-io/litter v1.5.5
 )
@@ -289,7 +290,6 @@ require (
 	github.com/pganalyze/pg_query_go/v2 v2.2.0 // indirect
 	github.com/pierrec/lz4 v2.0.5+incompatible // indirect
 	github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63 // indirect
-	github.com/pingcap/log v0.0.0-20210906054005-afc726e70354 // indirect
 	github.com/pingcap/tidb/parser v0.0.0-20220725134311-c80026e61f00 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/pquerna/cachecontrol v0.1.0 // indirect
diff --git a/internal/armada/configuration/types.go b/internal/armada/configuration/types.go
index 3e586361..18d6196b 100644
--- a/internal/armada/configuration/types.go
+++ b/internal/armada/configuration/types.go
@@ -23,18 +23,21 @@ type ArmadaConfig struct {
 
 	Grpc grpcconfig.GrpcConfig
 
-	PriorityHalfTime    time.Duration
-	CancelJobsBatchSize int
-	Redis               redis.UniversalOptions
-	EventsApiRedis      redis.UniversalOptions
-	Scheduling          SchedulingConfig
-	NewScheduler        NewSchedulerConfig
-	QueueManagement     QueueManagementConfig
-	DatabaseRetention   DatabaseRetentionPolicy
-	Pulsar              PulsarConfig
-	Postgres            PostgresConfig // Used for Pulsar submit API deduplication
-	EventApi            EventApiConfig
-	Metrics             MetricsConfig
+	PriorityHalfTime                  time.Duration
+	CancelJobsBatchSize               int
+	Redis                             redis.UniversalOptions
+	EventsApiRedis                    redis.UniversalOptions
+	Scheduling                        SchedulingConfig
+	NewScheduler                      NewSchedulerConfig
+	QueueManagement                   QueueManagementConfig
+	DatabaseRetention                 DatabaseRetentionPolicy
+	Pulsar                            PulsarConfig
+	Postgres                          PostgresConfig // Used for Pulsar submit API deduplication
+	EventApi                          EventApiConfig
+	Metrics                           MetricsConfig
+	IgnoreJobSubmitChecks             bool // Temporary flag to stop us rejecting jobs on switch over
+	PulsarSchedulerEnabled            bool
+	ProbabilityOfUsingPulsarScheduler float64
 }
 
 type PulsarConfig struct {
@@ -216,6 +219,14 @@ type PriorityClass struct {
 	MaximalResourceFractionPerQueue map[string]float64
 }
 
+func (p PreemptionConfig) AllowedPriorities() []int32 {
+	allowedPcs := make([]int32, 0, len(p.PriorityClasses))
+	for _, v := range p.PriorityClasses {
+		allowedPcs = append(allowedPcs, v.Priority)
+	}
+	return allowedPcs
+}
+
 type DatabaseRetentionPolicy struct {
 	JobRetentionDuration time.Duration
 }
diff --git a/internal/armada/repository/event_store.go b/internal/armada/repository/event_store.go
index 1dc2c6bd..db033770 100644
--- a/internal/armada/repository/event_store.go
+++ b/internal/armada/repository/event_store.go
@@ -7,6 +7,7 @@ import (
 
 	"github.com/armadaproject/armada/internal/common/eventutil"
 	"github.com/armadaproject/armada/internal/common/pulsarutils"
+	"github.com/armadaproject/armada/internal/common/schedulers"
 	"github.com/armadaproject/armada/pkg/api"
 )
 
@@ -54,5 +55,5 @@ func (n *StreamEventStore) ReportEvents(apiEvents []*api.EventMessage) error {
 	if err != nil {
 		return err
 	}
-	return pulsarutils.PublishSequences(context.Background(), n.Producer, sequences)
+	return pulsarutils.PublishSequences(context.Background(), n.Producer, sequences, schedulers.Legacy)
 }
diff --git a/internal/armada/repository/job.go b/internal/armada/repository/job.go
index 702a702f..6cccde03 100644
--- a/internal/armada/repository/job.go
+++ b/internal/armada/repository/job.go
@@ -14,7 +14,9 @@ import (
 
 	"github.com/armadaproject/armada/internal/armada/configuration"
 	"github.com/armadaproject/armada/internal/common/armadaerrors"
+	protoutil "github.com/armadaproject/armada/internal/common/proto"
 	"github.com/armadaproject/armada/internal/common/util"
+	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
 	"github.com/armadaproject/armada/pkg/api"
 )
 
@@ -29,6 +31,7 @@ const (
 	jobClientIdPrefix  = "job:ClientId:" // {queue}:{clientId} - corresponding jobId
 	jobExistsPrefix    = "Job:added"     // {jobId}            - flag to say we've added the job
 	keySeparator       = ":"
+	pulsarJobPrefix    = "PulsarJob:" // {jobId}            - pulsarjob protobuf object
 )
 
 type ErrJobNotFound struct {
@@ -80,6 +83,9 @@ type JobRepository interface {
 	GetQueueActiveJobSets(queue string) ([]*api.JobSetInfo, error)
 	AddRetryAttempt(jobId string) error
 	GetNumberOfRetryAttempts(jobId string) (int, error)
+	StorePulsarSchedulerJobDetails(jobDetails []*schedulerobjects.PulsarSchedulerJobDetails) error
+	GetPulsarSchedulerJobDetails(jobIds string) (*schedulerobjects.PulsarSchedulerJobDetails, error)
+	DeletePulsarSchedulerJobDetails(jobId []string) error
 }
 
 type RedisJobRepository struct {
@@ -1017,6 +1023,53 @@ func (repo *RedisJobRepository) GetNumberOfRetryAttempts(jobId string) (int, err
 	return retries, nil
 }
 
+func (repo *RedisJobRepository) StorePulsarSchedulerJobDetails(jobDetails []*schedulerobjects.PulsarSchedulerJobDetails) error {
+	pipe := repo.db.Pipeline()
+	for _, job := range jobDetails {
+		key := fmt.Sprintf("%s%s", pulsarJobPrefix, job.JobId)
+		jobData, err := proto.Marshal(job)
+		if err != nil {
+			return errors.WithStack(err)
+		}
+		pipe.Set(key, jobData, 375*24*time.Hour) // expire after a year
+	}
+	_, err := pipe.Exec()
+	if err != nil {
+		return errors.Wrapf(err, "error storing pulsar job details in redis")
+	}
+	return nil
+}
+
+func (repo *RedisJobRepository) GetPulsarSchedulerJobDetails(jobId string) (*schedulerobjects.PulsarSchedulerJobDetails, error) {
+	cmd := repo.db.Get(pulsarJobPrefix + jobId)
+
+	bytes, err := cmd.Bytes()
+	if err != nil && err != redis.Nil {
+		return nil, errors.Wrapf(err, "Errror retrieving job details for %s in redis", jobId)
+	}
+	if err == redis.Nil {
+		return nil, nil
+	}
+	details, err := protoutil.Unmarshall(bytes, &schedulerobjects.PulsarSchedulerJobDetails{})
+	if err != nil {
+		return nil, errors.Wrapf(err, "Errror unmarshalling job details for %s in redis", jobId)
+	}
+
+	return details, nil
+}
+
+func (repo *RedisJobRepository) DeletePulsarSchedulerJobDetails(jobIds []string) error {
+	pipe := repo.db.Pipeline()
+	for _, jobId := range jobIds {
+		pipe.Expire(pulsarJobPrefix+jobId, repo.retentionPolicy.JobRetentionDuration)
+	}
+	_, err := pipe.Exec()
+	if err != nil {
+		return errors.Wrap(err, "Error expiring pulsar job details in redis")
+	}
+	return nil
+}
+
 func (repo *RedisJobRepository) leaseJobs(clusterId string, jobIdsByQueue map[string][]string) (map[string][]string, error) {
 	now := time.Now()
 	pipe := repo.db.Pipeline()
diff --git a/internal/armada/server.go b/internal/armada/server.go
index de923919..52c996cb 100644
--- a/internal/armada/server.go
+++ b/internal/armada/server.go
@@ -32,6 +32,7 @@ import (
 	"github.com/armadaproject/armada/internal/common/task"
 	"github.com/armadaproject/armada/internal/common/util"
 	"github.com/armadaproject/armada/internal/scheduler"
+	schedulerdb "github.com/armadaproject/armada/internal/scheduler/database"
 	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
 	"github.com/armadaproject/armada/pkg/api"
 )
@@ -133,17 +134,29 @@ func Serve(ctx context.Context, config *configuration.ArmadaConfig, healthChecks
 		defer pool.Close()
 	}
 
-	// If Pulsar is enabled, use the Pulsar submit endpoints.
-	// Store a list of all Pulsar components to use during cleanup later.
-	var pulsarClient pulsar.Client
-	submitChecker := scheduler.NewSubmitChecker(
-		10*time.Minute,
-		config.Scheduling.Preemption.PriorityClasses,
-		config.Scheduling.GangIdAnnotation,
+	// Executor Repositories for pulsar and legacy schedulers respectively
+	pulsarExecutorRepo := schedulerdb.NewRedisExecutorRepository(db, "pulsar")
+	legacyExecutorRepo := schedulerdb.NewRedisExecutorRepository(db, "legacy")
+
+	pulsarSchedulerSubmitChecker := scheduler.NewSubmitChecker(
+		30*time.Minute,
+		config.Scheduling,
+		pulsarExecutorRepo,
 	)
+	services = append(services, func() error {
+		return pulsarSchedulerSubmitChecker.Run(ctx)
+	})
+	legacySchedulerSubmitChecker := scheduler.NewSubmitChecker(
+		30*time.Minute,
+		config.Scheduling,
+		legacyExecutorRepo,
+	)
+	services = append(services, func() error {
+		return legacySchedulerSubmitChecker.Run(ctx)
+	})
 
 	serverId := uuid.New()
-
+	var pulsarClient pulsar.Client
 	// API endpoints that generate Pulsar messages.
 	pulsarClient, err = pulsarutils.NewPulsarClient(&config.Pulsar)
 	if err != nil {
@@ -178,12 +191,18 @@ func Serve(ctx context.Context, config *configuration.ArmadaConfig, healthChecks
 	)
 
 	pulsarSubmitServer := &server.PulsarSubmitServer{
-		Producer:              producer,
-		QueueRepository:       queueRepository,
-		Permissions:           permissions,
-		SubmitServer:          submitServer,
-		MaxAllowedMessageSize: config.Pulsar.MaxAllowedMessageSize,
-		SubmitChecker:         submitChecker,
+		Producer:                           producer,
+		QueueRepository:                    queueRepository,
+		Permissions:                        permissions,
+		SubmitServer:                       submitServer,
+		MaxAllowedMessageSize:              config.Pulsar.MaxAllowedMessageSize,
+		PulsarSchedulerSubmitChecker:       pulsarSchedulerSubmitChecker,
+		LegacySchedulerSubmitChecker:       legacySchedulerSubmitChecker,
+		PulsarSchedulerEnabled:             config.PulsarSchedulerEnabled,
+		ProbabilityOdfUsingPulsarScheduler: config.ProbabilityOfUsingPulsarScheduler,
+		Rand:                               util.NewThreadsafeRand(time.Now().UnixNano()),
+		GangIdAnnotation:                   config.Scheduling.GangIdAnnotation,
+		IgnoreJobSubmitChecks:              config.IgnoreJobSubmitChecks,
 	}
 	submitServerToRegister := pulsarSubmitServer
 
@@ -249,8 +268,8 @@ func Serve(ctx context.Context, config *configuration.ArmadaConfig, healthChecks
 		usageRepository,
 		eventStore,
 		schedulingInfoRepository,
+		legacyExecutorRepo,
 	)
-	aggregatedQueueServer.SubmitChecker = submitChecker
 	if config.Scheduling.MaxQueueReportsToStore > 0 || config.Scheduling.MaxJobReportsToStore > 0 {
 		aggregatedQueueServer.SchedulingReportsRepository = scheduler.NewSchedulingReportsRepository(
 			config.Scheduling.MaxQueueReportsToStore,
diff --git a/internal/armada/server/lease.go b/internal/armada/server/lease.go
index 9ab79c9c..a9241cb2 100644
--- a/internal/armada/server/lease.go
+++ b/internal/armada/server/lease.go
@@ -30,6 +30,7 @@ import (
 	armadaresource "github.com/armadaproject/armada/internal/common/resource"
 	"github.com/armadaproject/armada/internal/common/util"
 	"github.com/armadaproject/armada/internal/scheduler"
+	"github.com/armadaproject/armada/internal/scheduler/database"
 	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
 	"github.com/armadaproject/armada/pkg/api"
 )
@@ -48,7 +49,7 @@ type AggregatedQueueServer struct {
 	SchedulingReportsRepository *scheduler.SchedulingReportsRepository
 	// Stores the most recent NodeDb for each executor.
 	// Used to check if a job could ever be scheduled at job submit time.
-	SubmitChecker *scheduler.SubmitChecker
+	executorRepository database.ExecutorRepository
 }
 
 func NewAggregatedQueueServer(
@@ -59,6 +60,7 @@ func NewAggregatedQueueServer(
 	usageRepository repository.UsageRepository,
 	eventStore repository.EventStore,
 	schedulingInfoRepository repository.SchedulingInfoRepository,
+	executorRepository database.ExecutorRepository,
 ) *AggregatedQueueServer {
 	poolConfig := pool.ObjectPoolConfig{
 		MaxTotal:                 100,
@@ -84,6 +86,7 @@ func NewAggregatedQueueServer(
 		eventStore:               eventStore,
 		schedulingInfoRepository: schedulingInfoRepository,
 		decompressorPool:         decompressorPool,
+		executorRepository:       executorRepository,
 		clock:                    clock.RealClock{},
 	}
 }
@@ -312,6 +315,21 @@ func (q *AggregatedQueueServer) getJobs(ctx context.Context, req *api.StreamingL
 		return nil, err
 	}
 
+	// Store the executor details in redis so that they can be used by
+	// submit check and the new scheduler.
+	err = q.executorRepository.StoreExecutor(ctx, &schedulerobjects.Executor{
+		Id:             req.ClusterId,
+		Pool:           req.Pool,
+		Nodes:          nodes,
+		MinimumJobSize: schedulerobjects.ResourceList{Resources: req.MinimumJobSize},
+		LastUpdateTime: time.Now(),
+	})
+
+	if err != nil {
+		// This is not fatal because we can still schedule if it doesn't happen
+		log.WithError(err).Warnf("Could not store executor details for cluster %s", req.ClusterId)
+	}
+
 	// Map queue names to priority factor for all active queues, i.e.,
 	// all queues for which the jobs queue has not been deleted automatically by Redis.
 	queues, err := q.queueRepository.GetAllQueues()
@@ -455,16 +473,6 @@ func (q *AggregatedQueueServer) getJobs(ctx context.Context, req *api.StreamingL
 	} else if err != nil {
 		return nil, err
 	}
-
-	// Use this NodeDb when checking if a job could ever be scheduled.
-	// We clear allocated resources since we want to check if a job
-	// could be scheduled if the cluster was empty.
-	if err := nodeDb.ClearAllocated(); err == nil {
-		q.SubmitChecker.RegisterNodeDb(req.ClusterId, nodeDb)
-	} else {
-		logging.WithStacktrace(log, err).Error("failed to clear allocated resources in NodeDb")
-	}
-
 	return apiJobs, nil
 }
 
diff --git a/internal/armada/server/reporting.go b/internal/armada/server/reporting.go
index ea823660..f165e371 100644
--- a/internal/armada/server/reporting.go
+++ b/internal/armada/server/reporting.go
@@ -4,7 +4,6 @@ import (
 	"fmt"
 	"time"
 
-	"github.com/pkg/errors"
 	log "github.com/sirupsen/logrus"
 
 	"github.com/armadaproject/armada/internal/armada/repository"
@@ -60,22 +59,6 @@ func reportDuplicateDetected(repository repository.EventStore, results []*reposi
 	return nil
 }
 
-func reportDuplicateFoundEvents(repository repository.EventStore, events []*api.JobDuplicateFoundEvent) error {
-	apiEvents := make([]*api.EventMessage, len(events))
-	for i, event := range events {
-		event, err := api.Wrap(event)
-		if err != nil {
-			return errors.WithStack(err)
-		}
-		apiEvents[i] = event
-	}
-	err := repository.ReportEvents(apiEvents)
-	if err != nil {
-		return errors.WithStack(err)
-	}
-	return nil
-}
-
 func reportSubmitted(repository repository.EventStore, jobs []*api.Job) error {
 	events := []*api.EventMessage{}
 	now := time.Now()
diff --git a/internal/armada/server/submit_from_log.go b/internal/armada/server/submit_from_log.go
index dfd18380..8ee4bf82 100644
--- a/internal/armada/server/submit_from_log.go
+++ b/internal/armada/server/submit_from_log.go
@@ -20,6 +20,7 @@ import (
 	"github.com/armadaproject/armada/internal/common/logging"
 	"github.com/armadaproject/armada/internal/common/pulsarutils/pulsarrequestid"
 	"github.com/armadaproject/armada/internal/common/requestid"
+	"github.com/armadaproject/armada/internal/common/schedulers"
 	"github.com/armadaproject/armada/internal/common/util"
 	"github.com/armadaproject/armada/pkg/api"
 	"github.com/armadaproject/armada/pkg/armadaevents"
@@ -94,8 +95,9 @@ func (srv *SubmitFromLog) Run(ctx context.Context) error {
 		default:
 
 			// Get a message from Pulsar, which consists of a sequence of events (i.e., state transitions).
-			ctxWithTimeout, _ := context.WithTimeout(ctx, 10*time.Second)
+			ctxWithTimeout, cancel := context.WithTimeout(ctx, 10*time.Second)
 			msg, err := srv.Consumer.Receive(ctxWithTimeout)
+			cancel()
 			if errors.Is(err, context.DeadlineExceeded) {
 				break // expected
 			}
@@ -108,6 +110,13 @@ func (srv *SubmitFromLog) Run(ctx context.Context) error {
 				break
 			}
 
+			// If this message isn't for us we can simply ack it
+			// and go to the next message
+			if !schedulers.ForLegacyScheduler(msg) {
+				srv.Consumer.Ack(msg)
+				break
+			}
+
 			lastMessageId = msg.ID()
 			lastPublishTime = msg.PublishTime()
 			numReceived++
@@ -351,15 +360,11 @@ func (srv *SubmitFromLog) SubmitJobs(
 	jobSetName string,
 	es []*armadaevents.SubmitJob,
 ) (bool, error) {
-	// Filter out jobs not indented for this scheduler.
-	// This is the default scheduler, which is indicated by the empty string.
-	schedulerJobs := selectJobsForLegacyScheduler(es)
-
 	// Convert Pulsar jobs to legacy api jobs.
 	// We can't report job failure on error here, since the job failure message bundles the job struct.
 	// Hence, if an error occurs here, the job disappears from the point of view of the user.
 	// However, this code path is exercised when jobs are submitted to the log so errors should be rare.
-	jobs, err := eventutil.ApiJobsFromLogSubmitJobs(userId, groups, queueName, jobSetName, time.Now(), schedulerJobs)
+	jobs, err := eventutil.ApiJobsFromLogSubmitJobs(userId, groups, queueName, jobSetName, time.Now(), es)
 	if err != nil {
 		return true, err
 	}
diff --git a/internal/armada/server/submit_to_log.go b/internal/armada/server/submit_to_log.go
index 91547cb2..90a58e9b 100644
--- a/internal/armada/server/submit_to_log.go
+++ b/internal/armada/server/submit_to_log.go
@@ -4,12 +4,15 @@ import (
 	"context"
 	"crypto/sha1"
 	"fmt"
+	"math/rand"
 	"time"
 
 	"github.com/apache/pulsar-client-go/pulsar"
 	"github.com/gogo/protobuf/types"
+	"github.com/google/uuid"
 	"github.com/pkg/errors"
 	log "github.com/sirupsen/logrus"
+	"golang.org/x/exp/maps"
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/status"
 
@@ -21,7 +24,9 @@ import (
 	"github.com/armadaproject/armada/internal/common/auth/permission"
 	"github.com/armadaproject/armada/internal/common/eventutil"
 	"github.com/armadaproject/armada/internal/common/pgkeyvalue"
+	"github.com/armadaproject/armada/internal/common/pointer"
 	"github.com/armadaproject/armada/internal/common/pulsarutils"
+	"github.com/armadaproject/armada/internal/common/schedulers"
 	commonvalidation "github.com/armadaproject/armada/internal/common/validation"
 	"github.com/armadaproject/armada/internal/executor/configuration"
 	"github.com/armadaproject/armada/internal/scheduler"
@@ -45,9 +50,19 @@ type PulsarSubmitServer struct {
 	SubmitServer *SubmitServer
 	// Used for job submission deduplication.
 	KVStore *pgkeyvalue.PGKeyValueStore
-	// Used to check at job submit time if the job could ever be scheduled.
-	// Currently only used for gang jobs.
-	SubmitChecker *scheduler.SubmitChecker
+	// Used to check at job submit time if the job could ever be scheduled on either legacy or pulsar schedulers
+	PulsarSchedulerSubmitChecker *scheduler.SubmitChecker
+	LegacySchedulerSubmitChecker *scheduler.SubmitChecker
+	// Flag to control if we enable sending messages to the pulsar scheduler
+	PulsarSchedulerEnabled bool
+	// Probability of using the pulsar scheduler.  Has no effect if PulsarSchedulerEnabled is false
+	ProbabilityOdfUsingPulsarScheduler float64
+	// Used to assign a job to either legacy or pulsar schedulers. Injected here to allow repeatable tests
+	Rand *rand.Rand
+	// Gang id annotation. Needed because we cannot split a gang across schedulers.
+	GangIdAnnotation string
+	// Temporary flag to stop us rejecting jobs as we switch over to new submit checks
+	IgnoreJobSubmitChecks bool
 }
 
 func (srv *PulsarSubmitServer) SubmitJobs(ctx context.Context, req *api.JobSubmitRequest) (*api.JobSubmitResponse, error) {
@@ -57,7 +72,15 @@ func (srv *PulsarSubmitServer) SubmitJobs(ctx context.Context, req *api.JobSubmi
 	}
 
 	// Prepare an event sequence to be submitted to the log
-	sequence := &armadaevents.EventSequence{
+	pulsarSchedulerEvents := &armadaevents.EventSequence{
+		Queue:      req.Queue,
+		JobSetName: req.JobSetId,
+		UserId:     userId,
+		Groups:     groups,
+		Events:     make([]*armadaevents.EventSequence_Event, 0, len(req.JobRequestItems)),
+	}
+
+	legacySchedulerEvents := &armadaevents.EventSequence{
 		Queue:      req.Queue,
 		JobSetName: req.JobSetId,
 		UserId:     userId,
@@ -75,6 +98,11 @@ func (srv *PulsarSubmitServer) SubmitJobs(ctx context.Context, req *api.JobSubmi
 		return nil, err
 	}
 
+	schedulersByJobId, err := srv.assignScheduler(apiJobs)
+	if err != nil {
+		return nil, err
+	}
+
 	// Convert the API jobs to log jobs.
 	responses := make([]*api.JobSubmitResponseItem, len(req.JobRequestItems), len(req.JobRequestItems))
 
@@ -82,35 +110,18 @@ func (srv *PulsarSubmitServer) SubmitJobs(ctx context.Context, req *api.JobSubmi
 	if err != nil {
 		return nil, err
 	}
-	jobDuplicateFoundEvents := make([]*api.JobDuplicateFoundEvent, 0)
+
 	for i, apiJob := range apiJobs {
-		responses[i] = &api.JobSubmitResponseItem{
-			JobId: apiJob.GetId(),
+		eventTime := time.Now()
+		assignedScheduler, ok := schedulersByJobId[apiJob.Id]
+		if !ok {
+			// This should never happen as if we can't find a scheduler we would have errored earlier
+			return nil, errors.Errorf("Didn't allocate a scheduler for job %s", apiJob.Id)
 		}
 
-		// If a ClientId (i.e., a deduplication id) is provided,
-		// check for previous job submissions with the ClientId for this queue.
-		// If we find a duplicate, insert the previous jobId in the corresponding response
-		// and generate a job duplicate found event.
-		originalId, found := originalIds[apiJob.GetId()]
-		if apiJob.ClientId != "" && originalId != apiJob.GetId() {
-			if found && originalId != "" {
-				jobDuplicateFoundEvents = append(jobDuplicateFoundEvents, &api.JobDuplicateFoundEvent{
-					JobId:         responses[i].JobId,
-					Queue:         req.Queue,
-					JobSetId:      req.JobSetId,
-					Created:       time.Now(),
-					OriginalJobId: originalIds[apiJob.GetId()],
-				})
-				responses[i].JobId = originalIds[apiJob.GetId()]
-				// The job shouldn't be submitted twice. Move on to the next job.
-				continue
-			} else {
-				log.Warnf(
-					"ClientId %s was supplied for job %s but no original jobId could be found.  Deduplication will not be applied",
-					apiJob.ClientId,
-					apiJob.GetId())
-			}
+		es := legacySchedulerEvents
+		if assignedScheduler == schedulers.Pulsar {
+			es = pulsarSchedulerEvents
 		}
 
 		// Users submit API-specific service and ingress objects.
@@ -125,6 +136,10 @@ func (srv *PulsarSubmitServer) SubmitJobs(ctx context.Context, req *api.JobSubmi
 			return nil, err
 		}
 
+		responses[i] = &api.JobSubmitResponseItem{
+			JobId: apiJob.GetId(),
+		}
+
 		// The log accept a different type of job.
 		logJob, err := eventutil.LogSubmitJobFromApiJob(apiJob)
 		if err != nil {
@@ -138,189 +153,136 @@ func (srv *PulsarSubmitServer) SubmitJobs(ctx context.Context, req *api.JobSubmi
 			return nil, err
 		}
 
-		sequence.Events = append(sequence.Events, &armadaevents.EventSequence_Event{
+		es.Events = append(es.Events, &armadaevents.EventSequence_Event{
+			Created: &eventTime,
 			Event: &armadaevents.EventSequence_Event_SubmitJob{
 				SubmitJob: logJob,
 			},
 		})
-	}
 
-	// Check if all jobs can be scheduled.
-	// This check uses the legacy resource reporting logic.
-	legacySchedulerJobs := selectApiJobsForLegacyScheduler(apiJobs)
-	if len(legacySchedulerJobs) > 0 {
-		allClusterSchedulingInfo, err := srv.SubmitServer.schedulingInfoRepository.GetClusterSchedulingInfo()
-		if err != nil {
-			err = errors.WithMessage(err, "error getting scheduling info")
-			return nil, err
-		}
-		if ok, err := validateJobsCanBeScheduled(legacySchedulerJobs, allClusterSchedulingInfo); !ok {
-			if err != nil {
-				return nil, errors.WithMessagef(err, "can't schedule job for user %s", userId)
+		// If a ClientId (i.e., a deduplication id) is provided,
+		// check for previous job submissions with the ClientId for this queue.
+		// If we find a duplicate, insert the previous jobId in the corresponding response
+		// and generate a job duplicate found event.
+		originalId, found := originalIds[apiJob.GetId()]
+		if apiJob.ClientId != "" && originalId != apiJob.GetId() {
+			if found && originalId != "" {
+				oldJobId, err := armadaevents.ProtoUuidFromUlidString(originalIds[apiJob.GetId()])
+				if err != nil {
+					return nil, status.Error(codes.Internal, "error marshalling oldJobId")
+				}
+				es.Events = append(es.Events, &armadaevents.EventSequence_Event{
+					Created: &eventTime,
+					Event: &armadaevents.EventSequence_Event_JobDuplicateDetected{
+						JobDuplicateDetected: &armadaevents.JobDuplicateDetected{
+							NewJobId: logJob.JobId,
+							OldJobId: oldJobId,
+						},
+					},
+				})
+				responses[i].JobId = originalIds[apiJob.GetId()]
+				// The job shouldn't be submitted twice. Move on to the next job.
+				continue
 			} else {
-				return nil, errors.Errorf("can't schedule job for user %s", userId)
+				log.Warnf(
+					"ClientId %s was supplied for job %s but no original jobId could be found.  Deduplication will not be applied",
+					apiJob.ClientId,
+					apiJob.GetId())
 			}
 		}
 	}
 
-	// Check if all jobs can be scheduled.
-	// This check uses the NodeDb of the new scheduler and
-	// can check if all jobs in a gang can go onto the same cluster.
-	if canSchedule, reason := srv.SubmitChecker.CheckApiJobs(apiJobs); !canSchedule {
-		return nil, status.Errorf(codes.InvalidArgument, "at least one job or gang is unschedulable:\n%s", reason)
-	}
-
-	// Create events marking the jobs as submitted
-	err = reportSubmitted(srv.SubmitServer.eventStore, apiJobs)
-	if err != nil {
-		return nil, status.Errorf(codes.Aborted, "[SubmitJobs] error getting submitted report: %s", err)
-	}
-
-	err = reportDuplicateFoundEvents(srv.SubmitServer.eventStore, jobDuplicateFoundEvents)
-	if err != nil {
-		return nil, status.Errorf(codes.Internal, "error reporting duplicates: %s", err)
-	}
-
-	err = srv.publishToPulsar(ctx, []*armadaevents.EventSequence{sequence})
-
-	if err != nil {
-		log.WithError(err).Error("failed send to Pulsar")
-		return nil, status.Error(codes.Internal, "Failed to send message")
-	}
-
-	return &api.JobSubmitResponse{JobResponseItems: responses}, nil
-}
-
-// selectApiJobsForLegacyScheduler return a slice composed of all jobs for which the scheduler field is empty.
-func selectApiJobsForLegacyScheduler(jobs []*api.Job) []*api.Job {
-	rv := make([]*api.Job, 0, len(jobs))
-	for _, job := range jobs {
-		if job.Scheduler == "" {
-			rv = append(rv, job)
+	if len(pulsarSchedulerEvents.Events) > 0 {
+		err = srv.publishToPulsar(ctx, []*armadaevents.EventSequence{pulsarSchedulerEvents}, schedulers.Pulsar)
+		if err != nil {
+			log.WithError(err).Error("failed send pulsar scheduler events to Pulsar")
+			return nil, status.Error(codes.Internal, "Failed to send message")
 		}
 	}
-	return rv
-}
 
-// selectJobsForLegacyScheduler return a slice composed of all jobs for which the scheduler field is empty.
-func selectJobsForLegacyScheduler(jobs []*armadaevents.SubmitJob) []*armadaevents.SubmitJob {
-	rv := make([]*armadaevents.SubmitJob, 0, len(jobs))
-	for _, job := range jobs {
-		if job.Scheduler == "" {
-			rv = append(rv, job)
+	if len(legacySchedulerEvents.Events) > 0 {
+		err = srv.publishToPulsar(ctx, []*armadaevents.EventSequence{legacySchedulerEvents}, schedulers.Legacy)
+		if err != nil {
+			log.WithError(err).Error("failed send legacy scheduler events to Pulsar")
+			return nil, status.Error(codes.Internal, "Failed to send message")
 		}
 	}
-	return rv
+
+	return &api.JobSubmitResponse{JobResponseItems: responses}, nil
 }
 
 func (srv *PulsarSubmitServer) CancelJobs(ctx context.Context, req *api.JobCancelRequest) (*api.CancellationResult, error) {
+	// separate code path for multiple jobs
 	if len(req.JobIds) > 0 {
 		return srv.cancelJobsByIdsQueueJobset(ctx, req.JobIds, req.Queue, req.JobSetId)
 	}
 
-	// If either queue or jobSetId is missing, we need to get those from Redis.
-	// This must be done before checking auth, since the auth check expects a queue.
-	// If both queue and jobSetId are provided, we assume that those are correct
-	// to make it possible to cancel jobs that have been submitted but not written to Redis yet.
-	if req.JobId != "" && (req.Queue == "" || req.JobSetId == "") {
-		jobs, err := srv.SubmitServer.jobRepository.GetJobsByIds([]string{req.JobId})
+	// Another separate code path for cancelling an entire job set
+	// TODO: We should deprecate this and move people over to CancelJobSet()
+	if req.JobId == "" {
+		log.Warnf("CancelJobs called for queue=%s and jobset=%s but with empty job id. Redirecting to CancelJobSet()", req.Queue, req.JobSetId)
+		_, err := srv.CancelJobSet(ctx, &api.JobSetCancelRequest{
+			Queue:    req.Queue,
+			JobSetId: req.JobSetId,
+		})
 		if err != nil {
 			return nil, err
 		}
-		if len(jobs) == 0 {
-			return nil, &armadaerrors.ErrNotFound{
-				Type:  "job",
-				Value: req.JobId,
-			}
-		}
-		if len(jobs) != 1 { // Internal error; should never happen.
-			return nil, fmt.Errorf("expected 1 job result, but got %v", jobs)
-		}
-		queue := jobs[0].Job.GetQueue()
-		jobSetId := jobs[0].Job.GetJobSetId()
-
-		// We allow clients to submit requests only containing a job id.
-		// For these requests, we need to populate the queue and jobSetId fields.
-		if req.Queue == "" {
-			req.Queue = queue
-		}
-		if req.JobSetId == "" {
-			req.JobSetId = jobSetId
-		}
-
-		// If both a job id and queue or jobsetId is provided, return ErrNotFound if they don't match,
-		// since the job could not be found for the provided queue/jobSetId.
-		if req.Queue != queue {
-			return nil, &armadaerrors.ErrNotFound{
-				Type:    "job",
-				Value:   req.JobId,
-				Message: fmt.Sprintf("job not found in queue %s, try waiting or setting queue/jobSetId explicitly", req.Queue),
-			}
-		}
-		if req.JobSetId != jobSetId {
-			return nil, &armadaerrors.ErrNotFound{
-				Type:    "job",
-				Value:   req.JobId,
-				Message: fmt.Sprintf("job not found in job set %s, try waiting or setting queue/jobSetId explicitly", req.JobSetId),
-			}
-		}
+		return &api.CancellationResult{
+			CancelledIds: []string{req.JobId}, // we return an empty string here which seems a bit nonsensical- but that's what the old code did!
+		}, nil
 	}
 
-	userId, groups, err := srv.Authorize(ctx, req.Queue, permissions.CancelAnyJobs, queue.PermissionVerbCancel)
+	// resolve the queue and jobset of the job: we can't trust what the user has given us
+	resolvedQueue, resolvedJobset, err := srv.resolveQueueAndJobsetForJob(req.JobId)
 	if err != nil {
 		return nil, err
 	}
 
-	if req.Queue == "" {
-		return nil, &armadaerrors.ErrInvalidArgument{
-			Name:    "Queue",
-			Value:   req.Queue,
-			Message: "queue is empty",
+	// If both a job id and queue or jobsetId is provided, return ErrNotFound if they don't match,
+	// since the job could not be found for the provided queue/jobSetId.
+	if req.Queue != "" && req.Queue != resolvedQueue {
+		return nil, &armadaerrors.ErrNotFound{
+			Type:    "job",
+			Value:   req.JobId,
+			Message: fmt.Sprintf("job not found in queue %s, try waiting", req.Queue),
 		}
 	}
-	if req.JobSetId == "" {
-		return nil, &armadaerrors.ErrInvalidArgument{
-			Name:    "JobSetId",
-			Value:   req.JobSetId,
-			Message: "JobSetId is empty",
+	if req.JobSetId != "" && req.JobSetId != resolvedJobset {
+		return nil, &armadaerrors.ErrNotFound{
+			Type:    "job",
+			Value:   req.JobId,
+			Message: fmt.Sprintf("job not found in job set %s, try waiting", req.JobSetId),
 		}
 	}
 
-	var cancelledIds []string
+	userId, groups, err := srv.Authorize(ctx, resolvedQueue, permissions.CancelAnyJobs, queue.PermissionVerbCancel)
+	if err != nil {
+		return nil, err
+	}
+
+	jobId, err := armadaevents.ProtoUuidFromUlidString(req.JobId)
+	if err != nil {
+		return nil, err
+	}
 
 	sequence := &armadaevents.EventSequence{
-		Queue:      req.Queue,
-		JobSetName: req.JobSetId,
+		Queue:      resolvedQueue,
+		JobSetName: resolvedJobset,
 		UserId:     userId,
 		Groups:     groups,
-		Events:     make([]*armadaevents.EventSequence_Event, 1, 1),
-	}
-
-	// Empty JobId indicates that all jobs in the job set should be cancelled.
-	if req.JobId == "" {
-		sequence.Events[0] = &armadaevents.EventSequence_Event{
-			Event: &armadaevents.EventSequence_Event_CancelJobSet{
-				CancelJobSet: &armadaevents.CancelJobSet{},
-			},
-		}
-
-		cancelledIds = []string{fmt.Sprintf("all jobs in job set %s", req.JobSetId)}
-	} else {
-		jobId, err := armadaevents.ProtoUuidFromUlidString(req.JobId)
-		if err != nil {
-			return nil, err
-		}
-
-		sequence.Events[0] = &armadaevents.EventSequence_Event{
-			Event: &armadaevents.EventSequence_Event_CancelJob{
-				CancelJob: &armadaevents.CancelJob{JobId: jobId},
+		Events: []*armadaevents.EventSequence_Event{
+			{
+				Created: pointer.Now(),
+				Event: &armadaevents.EventSequence_Event_CancelJob{
+					CancelJob: &armadaevents.CancelJob{JobId: jobId},
+				},
 			},
-		}
-
-		cancelledIds = []string{req.JobId} // indicates no error
+		},
 	}
 
-	err = srv.publishToPulsar(ctx, []*armadaevents.EventSequence{sequence})
+	// we can send the message to cancel to both schedulers. If the scheduler it doesn't belong to it'll be a no-op
+	err = srv.publishToPulsar(ctx, []*armadaevents.EventSequence{sequence}, schedulers.All)
 
 	if err != nil {
 		log.WithError(err).Error("failed send to Pulsar")
@@ -328,7 +290,7 @@ func (srv *PulsarSubmitServer) CancelJobs(ctx context.Context, req *api.JobCance
 	}
 
 	return &api.CancellationResult{
-		CancelledIds: cancelledIds,
+		CancelledIds: []string{req.JobId}, // indicates no error
 	}, nil
 }
 
@@ -354,7 +316,8 @@ func (srv *PulsarSubmitServer) cancelJobsByIdsQueueJobset(ctx context.Context, j
 	}
 	var cancelledIds []string
 	sequence, cancelledIds := eventSequenceForJobIds(jobIds, q, jobSet, userId, groups)
-	err = srv.publishToPulsar(ctx, []*armadaevents.EventSequence{sequence})
+	// send the message to both schedulers because jobs may be on either
+	err = srv.publishToPulsar(ctx, []*armadaevents.EventSequence{sequence}, schedulers.All)
 	if err != nil {
 		log.WithError(err).Error("failed send to Pulsar")
 		return nil, status.Error(codes.Internal, "Failed to send message")
@@ -382,6 +345,7 @@ func eventSequenceForJobIds(jobIds []string, q, jobSet, userId string, groups []
 		}
 		validIds = append(validIds, jobIdStr)
 		sequence.Events = append(sequence.Events, &armadaevents.EventSequence_Event{
+			Created: pointer.Now(),
 			Event: &armadaevents.EventSequence_Event_CancelJob{
 				CancelJob: &armadaevents.CancelJob{JobId: jobId},
 			},
@@ -395,14 +359,14 @@ func (srv *PulsarSubmitServer) CancelJobSet(ctx context.Context, req *api.JobSet
 		return nil, &armadaerrors.ErrInvalidArgument{
 			Name:    "Queue",
 			Value:   req.Queue,
-			Message: "queue is empty",
+			Message: "queue cannot be empty when cancelling a jobset",
 		}
 	}
 	if req.JobSetId == "" {
 		return nil, &armadaerrors.ErrInvalidArgument{
 			Name:    "JobSetId",
 			Value:   req.JobSetId,
-			Message: "JobSetId is empty",
+			Message: "jobsetId cannot be empty when cancelling a jobset",
 		}
 	}
 
@@ -416,12 +380,13 @@ func (srv *PulsarSubmitServer) CancelJobSet(ctx context.Context, req *api.JobSet
 		return nil, err
 	}
 
+	// We don't know if the jobs are allocated to the legacy scheduler or the new scheduler.  We therefore send messages to both
 	ids, err := srv.SubmitServer.jobRepository.GetJobSetJobIds(req.Queue, req.JobSetId, createJobSetFilter(req.Filter))
 	if err != nil {
 		return nil, status.Errorf(codes.Unavailable, "error getting job IDs: %s", err)
 	}
 
-	sequence := &armadaevents.EventSequence{
+	legacySchedulerSequence := &armadaevents.EventSequence{
 		Queue:      req.Queue,
 		JobSetName: req.JobSetId,
 		UserId:     userId,
@@ -435,20 +400,55 @@ func (srv *PulsarSubmitServer) CancelJobSet(ctx context.Context, req *api.JobSet
 			return nil, err
 		}
 
-		sequence.Events = append(sequence.Events, &armadaevents.EventSequence_Event{
+		legacySchedulerSequence.Events = append(legacySchedulerSequence.Events, &armadaevents.EventSequence_Event{
+			Created: pointer.Now(),
 			Event: &armadaevents.EventSequence_Event_CancelJob{
 				CancelJob: &armadaevents.CancelJob{JobId: jobId},
 			},
 		})
 	}
 
-	err = srv.publishToPulsar(ctx, []*armadaevents.EventSequence{sequence})
-
+	err = srv.publishToPulsar(ctx, []*armadaevents.EventSequence{legacySchedulerSequence}, schedulers.Legacy)
 	if err != nil {
 		log.WithError(err).Error("failed to send cancel job messages to pulsar")
 		return nil, status.Error(codes.Internal, "failed to send cancel job messages to pulsar")
 	}
 
+	if srv.PulsarSchedulerEnabled {
+		states := make([]armadaevents.JobState, len(req.GetFilter().GetStates()))
+		for i := 0; i < len(states); i++ {
+			switch req.GetFilter().GetStates()[i] {
+			case api.JobState_PENDING:
+				states[i] = armadaevents.JobState_PENDING
+			case api.JobState_QUEUED:
+				states[i] = armadaevents.JobState_QUEUED
+			case api.JobState_RUNNING:
+				states[i] = armadaevents.JobState_RUNNING
+			}
+		}
+		pulsarSchedulerSequence := &armadaevents.EventSequence{
+			Queue:      req.Queue,
+			JobSetName: req.JobSetId,
+			UserId:     userId,
+			Groups:     groups,
+			Events: []*armadaevents.EventSequence_Event{
+				{
+					Created: pointer.Now(),
+					Event: &armadaevents.EventSequence_Event_CancelJobSet{
+						CancelJobSet: &armadaevents.CancelJobSet{
+							States: states,
+						},
+					},
+				},
+			},
+		}
+		err = srv.publishToPulsar(ctx, []*armadaevents.EventSequence{pulsarSchedulerSequence}, schedulers.Pulsar)
+		if err != nil {
+			log.WithError(err).Error("failed to send cancel jobset message to pulsar")
+			return nil, status.Error(codes.Internal, "failed to send cancel jobset message to pulsar")
+		}
+	}
+
 	return &types.Empty{}, err
 }
 
@@ -457,54 +457,37 @@ func (srv *PulsarSubmitServer) ReprioritizeJobs(ctx context.Context, req *api.Jo
 	// with the first job id in the request.
 	//
 	// This must be done before checking auth, since the auth check expects a queue.
-	// If both queue and jobSetId are provided, we assume that those are correct
-	// to make it possible to cancel jobs that have been submitted but not written to Redis yet.
 	if len(req.JobIds) > 0 && (req.Queue == "" || req.JobSetId == "") {
 		firstJobId := req.JobIds[0]
 
-		jobs, err := srv.SubmitServer.jobRepository.GetJobsByIds([]string{firstJobId})
+		resolvedQueue, resolvedJobset, err := srv.resolveQueueAndJobsetForJob(firstJobId)
 		if err != nil {
 			return nil, err
 		}
-		if len(jobs) == 0 {
-			return nil, &armadaerrors.ErrNotFound{
-				Type:  "job",
-				Value: firstJobId,
-			}
-		}
-		if len(jobs) != 1 { // Internal error; should never happen.
-			return nil, fmt.Errorf("expected 1 job result, but got %v", jobs)
-		}
-		queue := jobs[0].Job.GetQueue()
-		jobSetId := jobs[0].Job.GetJobSetId()
-
-		// We allow clients to submit requests only containing a job id.
-		// For these requests, we need to populate the queue and jobSetId fields.
-		if req.Queue == "" {
-			req.Queue = queue
-		}
-		if req.JobSetId == "" {
-			req.JobSetId = jobSetId
-		}
 
 		// If both a job id and queue or jobsetId is provided, return ErrNotFound if they don't match,
 		// since the job could not be found for the provided queue/jobSetId.
-		if req.Queue != queue {
+		// If both a job id and queue or jobsetId is provided, return ErrNotFound if they don't match,
+		// since the job could not be found for the provided queue/jobSetId.
+		if req.Queue != "" && req.Queue != resolvedQueue {
 			return nil, &armadaerrors.ErrNotFound{
 				Type:    "job",
 				Value:   firstJobId,
-				Message: fmt.Sprintf("job not found in queue %s, try waiting or setting queue/jobSetId explicitly", req.Queue),
+				Message: fmt.Sprintf("job not found in queue %s, try waiting", req.Queue),
 			}
 		}
-		if req.JobSetId != jobSetId {
+		if req.JobSetId != "" && req.JobSetId != resolvedJobset {
 			return nil, &armadaerrors.ErrNotFound{
 				Type:    "job",
 				Value:   firstJobId,
-				Message: fmt.Sprintf("job not found in job set %s, try waiting or setting queue/jobSetId explicitly", req.JobSetId),
+				Message: fmt.Sprintf("job not found in job set %s, try waiting", req.JobSetId),
 			}
 		}
+		req.Queue = resolvedQueue
+		req.JobSetId = resolvedJobset
 	}
 
+	// TODO: this is incorrect we only validate the permissions on the first job but the other jobs may belong to different queues
 	userId, groups, err := srv.Authorize(ctx, req.Queue, permissions.ReprioritizeAnyJobs, queue.PermissionVerbReprioritize)
 	if err != nil {
 		return nil, err
@@ -535,7 +518,7 @@ func (srv *PulsarSubmitServer) ReprioritizeJobs(ctx context.Context, req *api.Jo
 
 	sequence := &armadaevents.EventSequence{
 		Queue:      req.Queue,
-		JobSetName: req.JobSetId,
+		JobSetName: req.JobSetId, // TODO: this is incorrect- the jobs may be for different jobsets
 		UserId:     userId,
 		Groups:     groups,
 		Events:     make([]*armadaevents.EventSequence_Event, len(req.JobIds), len(req.JobIds)),
@@ -575,7 +558,8 @@ func (srv *PulsarSubmitServer) ReprioritizeJobs(ctx context.Context, req *api.Jo
 		results[jobIdString] = "" // empty string indicates no error
 	}
 
-	err = srv.publishToPulsar(ctx, []*armadaevents.EventSequence{sequence})
+	// can send the message to both schedulers
+	err = srv.publishToPulsar(ctx, []*armadaevents.EventSequence{sequence}, schedulers.All)
 
 	if err != nil {
 		log.WithError(err).Error("failed send to Pulsar")
@@ -673,23 +657,8 @@ func (srv *PulsarSubmitServer) GetQueueInfo(ctx context.Context, req *api.QueueI
 	return srv.SubmitServer.GetQueueInfo(ctx, req)
 }
 
-// SubmitApiEvents converts several api.EventMessage into Pulsar state transition messages and publishes those to Pulsar.
-func (srv *PulsarSubmitServer) SubmitApiEvents(ctx context.Context, apiEvents []*api.EventMessage) error {
-	// Because (queue, userId, jobSetId) may differ between events,
-	// several sequences may be necessary.
-	sequences, err := eventutil.EventSequencesFromApiEvents(apiEvents)
-	if err != nil {
-		return err
-	}
-	if len(sequences) == 0 {
-		return nil
-	}
-
-	return srv.publishToPulsar(ctx, sequences)
-}
-
 // PublishToPulsar sends pulsar messages async
-func (srv *PulsarSubmitServer) publishToPulsar(ctx context.Context, sequences []*armadaevents.EventSequence) error {
+func (srv *PulsarSubmitServer) publishToPulsar(ctx context.Context, sequences []*armadaevents.EventSequence, scheduler schedulers.Scheduler) error {
 	// Reduce the number of sequences to send to the minimum possible,
 	// and then break up any sequences larger than srv.MaxAllowedMessageSize.
 	sequences = eventutil.CompactEventSequences(sequences)
@@ -697,7 +666,7 @@ func (srv *PulsarSubmitServer) publishToPulsar(ctx context.Context, sequences []
 	if err != nil {
 		return err
 	}
-	return pulsarutils.PublishSequences(ctx, srv.Producer, sequences)
+	return pulsarutils.PublishSequences(ctx, srv.Producer, sequences, scheduler)
 }
 
 // getOriginalJobIds returns the mapping between jobId and originalJobId.  If the job (or more specifically the clientId
@@ -751,3 +720,101 @@ func (srv *PulsarSubmitServer) getOriginalJobIds(ctx context.Context, apiJobs []
 
 	return nil, nil
 }
+
+// assignScheduler Assign a slice of jobs to either the legacy or pulsar schedulers.  This done by checking whether each
+// job can be scheduled on either scheduler.  If the job can only be scheduled on only one of the schedulers, it is
+// assigned to that scheduler. If it can be assigned to both scheduler it is assigned randomly based on
+// ProbabilityOdfUsingPulsarScheduler.  If it can be assigned on neither scheduler then an error is returned.
+func (srv *PulsarSubmitServer) assignScheduler(jobs []*api.Job) (map[string]schedulers.Scheduler, error) {
+	// when assigning jobs to a scheduler, all the jobs in a gang have to go on the same scheduler
+	groups := srv.groupJobsByGangId(jobs)
+	assignedSchedulers := make(map[string]schedulers.Scheduler, len(jobs))
+	for _, group := range groups {
+		schedulableOnLegacyScheduler, legacyMsg := srv.LegacySchedulerSubmitChecker.CheckApiJobs(group)
+		schedulableOnLegacyScheduler = schedulableOnLegacyScheduler || srv.IgnoreJobSubmitChecks
+		schedulableOnPulsarScheduler := false
+		pulsarMsg := ""
+		if srv.PulsarSchedulerEnabled {
+			schedulableOnPulsarScheduler, pulsarMsg = srv.PulsarSchedulerSubmitChecker.CheckApiJobs(group)
+			schedulableOnPulsarScheduler = schedulableOnPulsarScheduler || srv.IgnoreJobSubmitChecks
+		}
+
+		// Not schedulable anywhere!
+		if !schedulableOnLegacyScheduler && !schedulableOnPulsarScheduler {
+			msg := fmt.Sprintf("Could not schedule on legacy scheduler because: %s", legacyMsg)
+			if srv.PulsarSchedulerEnabled {
+				msg = fmt.Sprintf("%s\nCould not schedule on pulsar scheduler because: %s", msg, pulsarMsg)
+			}
+			return nil, errors.New(msg)
+		}
+
+		r := srv.Rand.Float64()
+		var assignedScheduler schedulers.Scheduler
+		if jobs[0].Scheduler == "pulsar" { // explicitly to pulsar.  I'm only checking the first job here, but as this is a debug option should be fine
+			assignedScheduler = schedulers.Pulsar
+		} else if jobs[0].Scheduler == "legacy" { // explicitly to legacy.  Again only check first job
+			assignedScheduler = schedulers.Legacy
+		} else if schedulableOnPulsarScheduler && !schedulableOnLegacyScheduler { // only schedulable on pulsar
+			assignedScheduler = schedulers.Pulsar
+		} else if schedulableOnLegacyScheduler && !schedulableOnPulsarScheduler { // only schedulable on legacy
+			assignedScheduler = schedulers.Legacy
+		} else if r < srv.ProbabilityOdfUsingPulsarScheduler { // probabilistic routing to pulsar
+			assignedScheduler = schedulers.Pulsar
+		} else { // probabilistic routing to legacy
+			assignedScheduler = schedulers.Legacy
+		}
+		for _, job := range group {
+			assignedSchedulers[job.Id] = assignedScheduler
+		}
+	}
+	return assignedSchedulers, nil
+}
+
+// group all the jobs by  gang id.  If no gang annotation is present then they will be put into a group of 1
+func (srv *PulsarSubmitServer) groupJobsByGangId(jobs []*api.Job) [][]*api.Job {
+	rv := make(map[string][]*api.Job)
+	for _, job := range jobs {
+		groupId := uuid.NewString()
+		if len(job.Annotations) == 0 {
+			rv[groupId] = append(rv[groupId], job)
+		} else {
+			value := job.Annotations[srv.GangIdAnnotation]
+			if value == "" {
+				rv[groupId] = append(rv[groupId], job)
+			}
+			rv[value] = append(rv[value], job)
+		}
+	}
+	return maps.Values(rv)
+}
+
+// resolveQueueAndJobsetForJob returns the queue and jobset for a job.
+// First we check the legacy scheduler jobs and then (if no job resolved and pulsar scheduler enabled) we check
+// the pulsar scheduler jobs.
+// If no job can be retrieved then an error is returned.
+func (srv *PulsarSubmitServer) resolveQueueAndJobsetForJob(jobId string) (string, string, error) {
+	// Check the legacy scheduler first
+	jobs, err := srv.SubmitServer.jobRepository.GetJobsByIds([]string{jobId})
+	if err != nil {
+		return "", "", err
+	}
+	if len(jobs) >= 1 {
+		return jobs[0].Job.GetQueue(), jobs[0].Job.GetJobSetId(), nil
+	}
+
+	// now check the pulsar scheduler
+	if srv.PulsarSchedulerEnabled {
+		jobDetails, err := srv.SubmitServer.jobRepository.GetPulsarSchedulerJobDetails(jobId)
+		if err != nil {
+			return "", "", err
+		}
+		if jobDetails != nil {
+			return jobDetails.Queue, jobDetails.JobSet, nil
+		}
+	}
+
+	return "", "", &armadaerrors.ErrNotFound{
+		Type:  "job",
+		Value: jobId,
+	}
+}
diff --git a/internal/common/ingest/ingestion_pipeline.go b/internal/common/ingest/ingestion_pipeline.go
index 8703b6eb..d522f299 100644
--- a/internal/common/ingest/ingestion_pipeline.go
+++ b/internal/common/ingest/ingestion_pipeline.go
@@ -43,7 +43,7 @@ type EventSequencesWithIds struct {
 	MessageIds     []pulsar.MessageID
 }
 
-// IngestionPipeline is an pipeline that reads message from pulsar and inserts them into a sink. The pipeline will
+// IngestionPipeline is a pipeline that reads message from pulsar and inserts them into a sink. The pipeline will
 // handle the following automatically:
 //   - Receiving messages from pulsar
 //   - Combining messages into batches for efficient processing
@@ -59,11 +59,13 @@ type IngestionPipeline[T HasPulsarMessageIds] struct {
 	pulsarSubscriptionName string
 	pulsarBatchSize        int
 	pulsarBatchDuration    time.Duration
+	msgFilter              func(msg pulsar.Message) bool
 	converter              InstructionConverter[T]
 	sink                   Sink[T]
 	consumer               pulsar.Consumer // for test purposes only
 }
 
+// NewIngestionPipeline creates an IngestionPipeline that processes all pulsar messages
 func NewIngestionPipeline[T HasPulsarMessageIds](
 	pulsarConfig configuration.PulsarConfig,
 	pulsarSubscriptionName string,
@@ -73,6 +75,32 @@ func NewIngestionPipeline[T HasPulsarMessageIds](
 	sink Sink[T],
 	metricsConfig configuration.MetricsConfig,
 	metrics *commonmetrics.Metrics,
+) *IngestionPipeline[T] {
+	return NewFilteredMsgIngestionPipeline[T](
+		pulsarConfig,
+		pulsarSubscriptionName,
+		pulsarBatchSize,
+		pulsarBatchDuration,
+		func(_ pulsar.Message) bool { return true },
+		converter,
+		sink,
+		metricsConfig,
+		metrics,
+	)
+}
+
+// NewFilteredMsgIngestionPipeline creates an IngestionPipeline that processes only messages corresponding to the
+// supplied message filter
+func NewFilteredMsgIngestionPipeline[T HasPulsarMessageIds](
+	pulsarConfig configuration.PulsarConfig,
+	pulsarSubscriptionName string,
+	pulsarBatchSize int,
+	pulsarBatchDuration time.Duration,
+	msgFilter func(msg pulsar.Message) bool,
+	converter InstructionConverter[T],
+	sink Sink[T],
+	metricsConfig configuration.MetricsConfig,
+	metrics *commonmetrics.Metrics,
 ) *IngestionPipeline[T] {
 	return &IngestionPipeline[T]{
 		pulsarConfig:           pulsarConfig,
@@ -81,6 +109,7 @@ func NewIngestionPipeline[T HasPulsarMessageIds](
 		pulsarSubscriptionName: pulsarSubscriptionName,
 		pulsarBatchSize:        pulsarBatchSize,
 		pulsarBatchDuration:    pulsarBatchDuration,
+		msgFilter:              msgFilter,
 		converter:              converter,
 		sink:                   sink,
 	}
@@ -103,9 +132,14 @@ func (ingester *IngestionPipeline[T]) Run(ctx context.Context) error {
 		ingester.consumer = consumer
 		defer closePulsar()
 	}
-	pulsarMsgs := pulsarutils.Receive(ctx, ingester.consumer, ingester.pulsarConfig.ReceiveTimeout, ingester.pulsarConfig.BackoffTime, ingester.metrics)
+	pulsarMsgs := pulsarutils.Receive(
+		ctx,
+		ingester.consumer,
+		ingester.pulsarConfig.ReceiveTimeout,
+		ingester.pulsarConfig.BackoffTime,
+		ingester.metrics)
 
-	// Setup a context that n seconds after ctx
+	// Set up a context that n seconds after ctx
 	// This gives the rest of the pipeline a chance to flush pending messages
 	pipelineShutdownContext, cancel := context.WithCancel(context.Background())
 	go func() {
@@ -131,7 +165,7 @@ func (ingester *IngestionPipeline[T]) Run(ctx context.Context) error {
 	eventSequences := make(chan *EventSequencesWithIds)
 	go func() {
 		for msg := range batchedMsgs {
-			converted := unmarshalEventSequences(msg, ingester.metrics)
+			converted := unmarshalEventSequences(msg, ingester.msgFilter, ingester.metrics)
 			eventSequences <- converted
 		}
 		close(eventSequences)
@@ -202,7 +236,7 @@ func (ingester *IngestionPipeline[T]) subscribe() (pulsar.Consumer, func(), erro
 	}, nil
 }
 
-func unmarshalEventSequences(batch []pulsar.Message, metrics *commonmetrics.Metrics) *EventSequencesWithIds {
+func unmarshalEventSequences(batch []pulsar.Message, msgFilter func(msg pulsar.Message) bool, metrics *commonmetrics.Metrics) *EventSequencesWithIds {
 	sequences := make([]*armadaevents.EventSequence, 0, len(batch))
 	messageIds := make([]pulsar.MessageID, len(batch))
 	for i, msg := range batch {
@@ -211,8 +245,8 @@ func unmarshalEventSequences(batch []pulsar.Message, metrics *commonmetrics.Metr
 		// As they must be acked at the end
 		messageIds[i] = msg.ID()
 
-		// If it's not a control message then ignore
-		if !armadaevents.IsControlMessage(msg) {
+		// If we're not interested in this then continue
+		if !msgFilter(msg) {
 			continue
 		}
 
diff --git a/internal/common/mocks/generate.go b/internal/common/mocks/generate.go
new file mode 100644
index 00000000..72dd5ad4
--- /dev/null
+++ b/internal/common/mocks/generate.go
@@ -0,0 +1,4 @@
+package mocks
+
+// Mock implementations used by tests
+//go:generate mockgen -destination=./mock_pulsar.go -package=mocks "github.com/apache/pulsar-client-go/pulsar" Client,Producer,Message
diff --git a/internal/common/mocks/mock_pulsar.go b/internal/common/mocks/mock_pulsar.go
new file mode 100644
index 00000000..d362ffce
--- /dev/null
+++ b/internal/common/mocks/mock_pulsar.go
@@ -0,0 +1,489 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/apache/pulsar-client-go/pulsar (interfaces: Client,Producer,Message)
+
+// Package mocks is a generated GoMock package.
+package mocks
+
+import (
+	context "context"
+	reflect "reflect"
+	time "time"
+
+	pulsar "github.com/apache/pulsar-client-go/pulsar"
+	gomock "github.com/golang/mock/gomock"
+)
+
+// MockClient is a mock of Client interface.
+type MockClient struct {
+	ctrl     *gomock.Controller
+	recorder *MockClientMockRecorder
+}
+
+// MockClientMockRecorder is the mock recorder for MockClient.
+type MockClientMockRecorder struct {
+	mock *MockClient
+}
+
+// NewMockClient creates a new mock instance.
+func NewMockClient(ctrl *gomock.Controller) *MockClient {
+	mock := &MockClient{ctrl: ctrl}
+	mock.recorder = &MockClientMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockClient) EXPECT() *MockClientMockRecorder {
+	return m.recorder
+}
+
+// Close mocks base method.
+func (m *MockClient) Close() {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "Close")
+}
+
+// Close indicates an expected call of Close.
+func (mr *MockClientMockRecorder) Close() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockClient)(nil).Close))
+}
+
+// CreateProducer mocks base method.
+func (m *MockClient) CreateProducer(arg0 pulsar.ProducerOptions) (pulsar.Producer, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "CreateProducer", arg0)
+	ret0, _ := ret[0].(pulsar.Producer)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// CreateProducer indicates an expected call of CreateProducer.
+func (mr *MockClientMockRecorder) CreateProducer(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateProducer", reflect.TypeOf((*MockClient)(nil).CreateProducer), arg0)
+}
+
+// CreateReader mocks base method.
+func (m *MockClient) CreateReader(arg0 pulsar.ReaderOptions) (pulsar.Reader, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "CreateReader", arg0)
+	ret0, _ := ret[0].(pulsar.Reader)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// CreateReader indicates an expected call of CreateReader.
+func (mr *MockClientMockRecorder) CreateReader(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateReader", reflect.TypeOf((*MockClient)(nil).CreateReader), arg0)
+}
+
+// CreateTableView mocks base method.
+func (m *MockClient) CreateTableView(arg0 pulsar.TableViewOptions) (pulsar.TableView, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "CreateTableView", arg0)
+	ret0, _ := ret[0].(pulsar.TableView)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// CreateTableView indicates an expected call of CreateTableView.
+func (mr *MockClientMockRecorder) CreateTableView(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTableView", reflect.TypeOf((*MockClient)(nil).CreateTableView), arg0)
+}
+
+// Subscribe mocks base method.
+func (m *MockClient) Subscribe(arg0 pulsar.ConsumerOptions) (pulsar.Consumer, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Subscribe", arg0)
+	ret0, _ := ret[0].(pulsar.Consumer)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Subscribe indicates an expected call of Subscribe.
+func (mr *MockClientMockRecorder) Subscribe(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subscribe", reflect.TypeOf((*MockClient)(nil).Subscribe), arg0)
+}
+
+// TopicPartitions mocks base method.
+func (m *MockClient) TopicPartitions(arg0 string) ([]string, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "TopicPartitions", arg0)
+	ret0, _ := ret[0].([]string)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// TopicPartitions indicates an expected call of TopicPartitions.
+func (mr *MockClientMockRecorder) TopicPartitions(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TopicPartitions", reflect.TypeOf((*MockClient)(nil).TopicPartitions), arg0)
+}
+
+// MockProducer is a mock of Producer interface.
+type MockProducer struct {
+	ctrl     *gomock.Controller
+	recorder *MockProducerMockRecorder
+}
+
+// MockProducerMockRecorder is the mock recorder for MockProducer.
+type MockProducerMockRecorder struct {
+	mock *MockProducer
+}
+
+// NewMockProducer creates a new mock instance.
+func NewMockProducer(ctrl *gomock.Controller) *MockProducer {
+	mock := &MockProducer{ctrl: ctrl}
+	mock.recorder = &MockProducerMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockProducer) EXPECT() *MockProducerMockRecorder {
+	return m.recorder
+}
+
+// Close mocks base method.
+func (m *MockProducer) Close() {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "Close")
+}
+
+// Close indicates an expected call of Close.
+func (mr *MockProducerMockRecorder) Close() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockProducer)(nil).Close))
+}
+
+// Flush mocks base method.
+func (m *MockProducer) Flush() error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Flush")
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// Flush indicates an expected call of Flush.
+func (mr *MockProducerMockRecorder) Flush() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Flush", reflect.TypeOf((*MockProducer)(nil).Flush))
+}
+
+// LastSequenceID mocks base method.
+func (m *MockProducer) LastSequenceID() int64 {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "LastSequenceID")
+	ret0, _ := ret[0].(int64)
+	return ret0
+}
+
+// LastSequenceID indicates an expected call of LastSequenceID.
+func (mr *MockProducerMockRecorder) LastSequenceID() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LastSequenceID", reflect.TypeOf((*MockProducer)(nil).LastSequenceID))
+}
+
+// Name mocks base method.
+func (m *MockProducer) Name() string {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Name")
+	ret0, _ := ret[0].(string)
+	return ret0
+}
+
+// Name indicates an expected call of Name.
+func (mr *MockProducerMockRecorder) Name() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockProducer)(nil).Name))
+}
+
+// Send mocks base method.
+func (m *MockProducer) Send(arg0 context.Context, arg1 *pulsar.ProducerMessage) (pulsar.MessageID, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Send", arg0, arg1)
+	ret0, _ := ret[0].(pulsar.MessageID)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Send indicates an expected call of Send.
+func (mr *MockProducerMockRecorder) Send(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Send", reflect.TypeOf((*MockProducer)(nil).Send), arg0, arg1)
+}
+
+// SendAsync mocks base method.
+func (m *MockProducer) SendAsync(arg0 context.Context, arg1 *pulsar.ProducerMessage, arg2 func(pulsar.MessageID, *pulsar.ProducerMessage, error)) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "SendAsync", arg0, arg1, arg2)
+}
+
+// SendAsync indicates an expected call of SendAsync.
+func (mr *MockProducerMockRecorder) SendAsync(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendAsync", reflect.TypeOf((*MockProducer)(nil).SendAsync), arg0, arg1, arg2)
+}
+
+// Topic mocks base method.
+func (m *MockProducer) Topic() string {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Topic")
+	ret0, _ := ret[0].(string)
+	return ret0
+}
+
+// Topic indicates an expected call of Topic.
+func (mr *MockProducerMockRecorder) Topic() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Topic", reflect.TypeOf((*MockProducer)(nil).Topic))
+}
+
+// MockMessage is a mock of Message interface.
+type MockMessage struct {
+	ctrl     *gomock.Controller
+	recorder *MockMessageMockRecorder
+}
+
+// MockMessageMockRecorder is the mock recorder for MockMessage.
+type MockMessageMockRecorder struct {
+	mock *MockMessage
+}
+
+// NewMockMessage creates a new mock instance.
+func NewMockMessage(ctrl *gomock.Controller) *MockMessage {
+	mock := &MockMessage{ctrl: ctrl}
+	mock.recorder = &MockMessageMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockMessage) EXPECT() *MockMessageMockRecorder {
+	return m.recorder
+}
+
+// BrokerPublishTime mocks base method.
+func (m *MockMessage) BrokerPublishTime() *time.Time {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "BrokerPublishTime")
+	ret0, _ := ret[0].(*time.Time)
+	return ret0
+}
+
+// BrokerPublishTime indicates an expected call of BrokerPublishTime.
+func (mr *MockMessageMockRecorder) BrokerPublishTime() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BrokerPublishTime", reflect.TypeOf((*MockMessage)(nil).BrokerPublishTime))
+}
+
+// EventTime mocks base method.
+func (m *MockMessage) EventTime() time.Time {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "EventTime")
+	ret0, _ := ret[0].(time.Time)
+	return ret0
+}
+
+// EventTime indicates an expected call of EventTime.
+func (mr *MockMessageMockRecorder) EventTime() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EventTime", reflect.TypeOf((*MockMessage)(nil).EventTime))
+}
+
+// GetEncryptionContext mocks base method.
+func (m *MockMessage) GetEncryptionContext() *pulsar.EncryptionContext {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetEncryptionContext")
+	ret0, _ := ret[0].(*pulsar.EncryptionContext)
+	return ret0
+}
+
+// GetEncryptionContext indicates an expected call of GetEncryptionContext.
+func (mr *MockMessageMockRecorder) GetEncryptionContext() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEncryptionContext", reflect.TypeOf((*MockMessage)(nil).GetEncryptionContext))
+}
+
+// GetReplicatedFrom mocks base method.
+func (m *MockMessage) GetReplicatedFrom() string {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetReplicatedFrom")
+	ret0, _ := ret[0].(string)
+	return ret0
+}
+
+// GetReplicatedFrom indicates an expected call of GetReplicatedFrom.
+func (mr *MockMessageMockRecorder) GetReplicatedFrom() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReplicatedFrom", reflect.TypeOf((*MockMessage)(nil).GetReplicatedFrom))
+}
+
+// GetSchemaValue mocks base method.
+func (m *MockMessage) GetSchemaValue(arg0 interface{}) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetSchemaValue", arg0)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// GetSchemaValue indicates an expected call of GetSchemaValue.
+func (mr *MockMessageMockRecorder) GetSchemaValue(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSchemaValue", reflect.TypeOf((*MockMessage)(nil).GetSchemaValue), arg0)
+}
+
+// ID mocks base method.
+func (m *MockMessage) ID() pulsar.MessageID {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ID")
+	ret0, _ := ret[0].(pulsar.MessageID)
+	return ret0
+}
+
+// ID indicates an expected call of ID.
+func (mr *MockMessageMockRecorder) ID() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ID", reflect.TypeOf((*MockMessage)(nil).ID))
+}
+
+// Index mocks base method.
+func (m *MockMessage) Index() *uint64 {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Index")
+	ret0, _ := ret[0].(*uint64)
+	return ret0
+}
+
+// Index indicates an expected call of Index.
+func (mr *MockMessageMockRecorder) Index() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Index", reflect.TypeOf((*MockMessage)(nil).Index))
+}
+
+// IsReplicated mocks base method.
+func (m *MockMessage) IsReplicated() bool {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "IsReplicated")
+	ret0, _ := ret[0].(bool)
+	return ret0
+}
+
+// IsReplicated indicates an expected call of IsReplicated.
+func (mr *MockMessageMockRecorder) IsReplicated() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsReplicated", reflect.TypeOf((*MockMessage)(nil).IsReplicated))
+}
+
+// Key mocks base method.
+func (m *MockMessage) Key() string {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Key")
+	ret0, _ := ret[0].(string)
+	return ret0
+}
+
+// Key indicates an expected call of Key.
+func (mr *MockMessageMockRecorder) Key() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Key", reflect.TypeOf((*MockMessage)(nil).Key))
+}
+
+// OrderingKey mocks base method.
+func (m *MockMessage) OrderingKey() string {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "OrderingKey")
+	ret0, _ := ret[0].(string)
+	return ret0
+}
+
+// OrderingKey indicates an expected call of OrderingKey.
+func (mr *MockMessageMockRecorder) OrderingKey() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OrderingKey", reflect.TypeOf((*MockMessage)(nil).OrderingKey))
+}
+
+// Payload mocks base method.
+func (m *MockMessage) Payload() []byte {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Payload")
+	ret0, _ := ret[0].([]byte)
+	return ret0
+}
+
+// Payload indicates an expected call of Payload.
+func (mr *MockMessageMockRecorder) Payload() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Payload", reflect.TypeOf((*MockMessage)(nil).Payload))
+}
+
+// ProducerName mocks base method.
+func (m *MockMessage) ProducerName() string {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ProducerName")
+	ret0, _ := ret[0].(string)
+	return ret0
+}
+
+// ProducerName indicates an expected call of ProducerName.
+func (mr *MockMessageMockRecorder) ProducerName() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProducerName", reflect.TypeOf((*MockMessage)(nil).ProducerName))
+}
+
+// Properties mocks base method.
+func (m *MockMessage) Properties() map[string]string {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Properties")
+	ret0, _ := ret[0].(map[string]string)
+	return ret0
+}
+
+// Properties indicates an expected call of Properties.
+func (mr *MockMessageMockRecorder) Properties() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Properties", reflect.TypeOf((*MockMessage)(nil).Properties))
+}
+
+// PublishTime mocks base method.
+func (m *MockMessage) PublishTime() time.Time {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "PublishTime")
+	ret0, _ := ret[0].(time.Time)
+	return ret0
+}
+
+// PublishTime indicates an expected call of PublishTime.
+func (mr *MockMessageMockRecorder) PublishTime() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishTime", reflect.TypeOf((*MockMessage)(nil).PublishTime))
+}
+
+// RedeliveryCount mocks base method.
+func (m *MockMessage) RedeliveryCount() uint32 {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "RedeliveryCount")
+	ret0, _ := ret[0].(uint32)
+	return ret0
+}
+
+// RedeliveryCount indicates an expected call of RedeliveryCount.
+func (mr *MockMessageMockRecorder) RedeliveryCount() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RedeliveryCount", reflect.TypeOf((*MockMessage)(nil).RedeliveryCount))
+}
+
+// Topic mocks base method.
+func (m *MockMessage) Topic() string {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Topic")
+	ret0, _ := ret[0].(string)
+	return ret0
+}
+
+// Topic indicates an expected call of Topic.
+func (mr *MockMessageMockRecorder) Topic() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Topic", reflect.TypeOf((*MockMessage)(nil).Topic))
+}
diff --git a/internal/common/pointer/pointer.go b/internal/common/pointer/pointer.go
new file mode 100644
index 00000000..51b6596b
--- /dev/null
+++ b/internal/common/pointer/pointer.go
@@ -0,0 +1,13 @@
+package pointer
+
+import "time"
+
+// Now returns a pointer to the current time
+func Now() *time.Time {
+	return Time(time.Now())
+}
+
+// Time returns a pointer to supplied time
+func Time(t time.Time) *time.Time {
+	return &t
+}
diff --git a/internal/common/pulsarutils/async.go b/internal/common/pulsarutils/async.go
index e5f72139..06c99762 100644
--- a/internal/common/pulsarutils/async.go
+++ b/internal/common/pulsarutils/async.go
@@ -32,7 +32,7 @@ type ConsumerMessage struct {
 	ConsumerId int
 }
 
-var log = logrus.NewEntry(logrus.StandardLogger())
+var msgLogger = logrus.NewEntry(logrus.StandardLogger())
 
 func Receive(
 	ctx context.Context,
@@ -55,7 +55,7 @@ func Receive(
 		for {
 			// Periodic logging.
 			if time.Since(lastLogged) > logInterval {
-				log.WithFields(
+				msgLogger.WithFields(
 					logrus.Fields{
 						"received":      numReceived,
 						"interval":      logInterval,
@@ -70,7 +70,7 @@ func Receive(
 			// Exit if the context has been cancelled. Otherwise, get a message from Pulsar.
 			select {
 			case <-ctx.Done():
-				log.Infof("Shutting down pulsar receiver")
+				msgLogger.Infof("Shutting down pulsar receiver")
 				close(out)
 				return
 			default:
@@ -78,7 +78,7 @@ func Receive(
 				ctxWithTimeout, cancel := context.WithTimeout(ctx, receiveTimeout)
 				msg, err := consumer.Receive(ctxWithTimeout)
 				if errors.Is(err, context.DeadlineExceeded) {
-					log.Debugf("No message received")
+					msgLogger.Debugf("No message received")
 					cancel()
 					break // expected
 				}
@@ -88,7 +88,7 @@ func Receive(
 				if err != nil {
 					m.RecordPulsarConnectionError()
 					logging.
-						WithStacktrace(log, err).
+						WithStacktrace(msgLogger, err).
 						WithField("lastMessageId", lastMessageId).
 						Warnf("Pulsar receive failed; backing off for %s", backoffTime)
 					time.Sleep(backoffTime)
@@ -121,6 +121,6 @@ func Ack(ctx context.Context, consumers []pulsar.Consumer, msgs chan []*Consumer
 			consumers[id.ConsumerId].AckID(id.MessageId)
 		}
 	}
-	log.Info("Shutting down Ackker")
+	msgLogger.Info("Shutting down Ackker")
 	wg.Done()
 }
diff --git a/internal/common/pulsarutils/eventsequence.go b/internal/common/pulsarutils/eventsequence.go
index dc6316d9..09c61eb9 100644
--- a/internal/common/pulsarutils/eventsequence.go
+++ b/internal/common/pulsarutils/eventsequence.go
@@ -4,6 +4,8 @@ import (
 	"context"
 	"sync/atomic"
 
+	"github.com/armadaproject/armada/internal/common/schedulers"
+
 	"github.com/apache/pulsar-client-go/pulsar"
 	"github.com/gogo/protobuf/proto"
 	"github.com/hashicorp/go-multierror"
@@ -16,7 +18,7 @@ import (
 
 // CompactAndPublishSequences reduces the number of sequences to the smallest possible,
 // while respecting per-job set ordering and max Pulsar message size, and then publishes to Pulsar.
-func CompactAndPublishSequences(ctx context.Context, sequences []*armadaevents.EventSequence, producer pulsar.Producer, maxMessageSizeInBytes uint) error {
+func CompactAndPublishSequences(ctx context.Context, sequences []*armadaevents.EventSequence, producer pulsar.Producer, maxMessageSizeInBytes uint, scheduler schedulers.Scheduler) error {
 	// Reduce the number of sequences to send to the minimum possible,
 	// and then break up any sequences larger than maxMessageSizeInBytes.
 	sequences = eventutil.CompactEventSequences(sequences)
@@ -24,10 +26,10 @@ func CompactAndPublishSequences(ctx context.Context, sequences []*armadaevents.E
 	if err != nil {
 		return err
 	}
-	return PublishSequences(ctx, producer, sequences)
+	return PublishSequences(ctx, producer, sequences, scheduler)
 }
 
-// PublishSequence publishes several event sequences to Pulsar.
+// PublishSequences publishes several event sequences to Pulsar.
 // For efficiency, all sequences are queued for publishing and then flushed.
 // Returns once all sequences have been received by Pulsar.
 //
@@ -36,7 +38,7 @@ func CompactAndPublishSequences(ctx context.Context, sequences []*armadaevents.E
 // and
 // eventutil.LimitSequencesByteSize(sequences, int(srv.MaxAllowedMessageSize))
 // before passing to this function.
-func PublishSequences(ctx context.Context, producer pulsar.Producer, sequences []*armadaevents.EventSequence) error {
+func PublishSequences(ctx context.Context, producer pulsar.Producer, sequences []*armadaevents.EventSequence, scheduler schedulers.Scheduler) error {
 	// Incoming gRPC requests are annotated with a unique id.
 	// Pass this id through the log by adding it to the Pulsar message properties.
 	requestId := requestid.FromContextOrMissing(ctx)
@@ -68,8 +70,8 @@ func PublishSequences(ctx context.Context, producer pulsar.Producer, sequences [
 			&pulsar.ProducerMessage{
 				Payload: payloads[i],
 				Properties: map[string]string{
-					requestid.MetadataKey:                     requestId,
-					armadaevents.PULSAR_MESSAGE_TYPE_PROPERTY: armadaevents.PULSAR_CONTROL_MESSAGE,
+					requestid.MetadataKey:   requestId,
+					schedulers.PropertyName: schedulers.MsgPropertyFromScheduler(scheduler),
 				},
 				Key: sequences[i].JobSetName,
 			},
@@ -86,7 +88,7 @@ func PublishSequences(ctx context.Context, producer pulsar.Producer, sequences [
 		)
 	}
 
-	// Wait for all asynd send call to complete, collect any errors and return.
+	// Wait for all async send call to complete, collect any errors and return.
 	var result *multierror.Error
 	for range sequences {
 		select {
diff --git a/internal/common/schedulers/scheduler.go b/internal/common/schedulers/scheduler.go
new file mode 100644
index 00000000..476cc2d1
--- /dev/null
+++ b/internal/common/schedulers/scheduler.go
@@ -0,0 +1,62 @@
+package schedulers
+
+import (
+	"github.com/apache/pulsar-client-go/pulsar"
+	log "github.com/sirupsen/logrus"
+)
+
+type Scheduler int
+
+const (
+	Legacy Scheduler = iota
+	Pulsar
+	All
+)
+
+const (
+	PropertyName             string = "schedulerName"
+	PulsarSchedulerAttribute string = "pulsar"
+	LegacySchedulerAttribute string = "legacy"
+	AllSchedulersAttribute   string = "all"
+)
+
+// SchedulerFromMsg parses the message properties to retrieve the Scheduler associated with the message
+func SchedulerFromMsg(msg pulsar.Message) Scheduler {
+	s := msg.Properties()[PropertyName]
+	switch s {
+	case PulsarSchedulerAttribute:
+		return Pulsar
+	case LegacySchedulerAttribute, "": // empty string means legacy scheduler for compatibility
+		return Legacy
+	case AllSchedulersAttribute:
+		return All
+	}
+	log.Warnf("Unknown scheduler [%s] associated with pulsar message [%s]. Defaulting to legacy scheduler", s, msg.ID())
+	return Legacy
+}
+
+// MsgPropertyFromScheduler returns the pulsar message property associated with the scheduler
+func MsgPropertyFromScheduler(s Scheduler) string {
+	switch s {
+	case Pulsar:
+		return PulsarSchedulerAttribute
+	case Legacy:
+		return LegacySchedulerAttribute
+	case All:
+		return AllSchedulersAttribute
+	}
+	log.Warnf("Unknown scheduler [%d]. Defaulting to legacy scheduler", s)
+	return LegacySchedulerAttribute
+}
+
+// ForPulsarScheduler returns true if this message should be processed by the pulsar scheduler
+func ForPulsarScheduler(msg pulsar.Message) bool {
+	s := SchedulerFromMsg(msg)
+	return s == Pulsar || s == All
+}
+
+// ForLegacyScheduler returns true if this message should be processed by the legacy scheduler
+func ForLegacyScheduler(msg pulsar.Message) bool {
+	s := SchedulerFromMsg(msg)
+	return s == Legacy || s == All
+}
diff --git a/internal/scheduler/api.go b/internal/scheduler/api.go
index e0a28e31..0e04904b 100644
--- a/internal/scheduler/api.go
+++ b/internal/scheduler/api.go
@@ -4,6 +4,8 @@ import (
 	"context"
 	"strings"
 
+	"github.com/armadaproject/armada/internal/common/schedulers"
+
 	"github.com/apache/pulsar-client-go/pulsar"
 	"github.com/gogo/protobuf/proto"
 	"github.com/gogo/protobuf/types"
@@ -25,18 +27,20 @@ import (
 
 // ExecutorApi is a gRPC service that exposes functionality required by the armada executors
 type ExecutorApi struct {
-	producer             pulsar.Producer
-	jobRepository        database.JobRepository
-	executorRepository   database.ExecutorRepository
-	allowedPriorities    []int32 // allowed priority classes
-	maxJobsPerCall       uint    // maximum number of jobs that will be leased in a single call
-	maxPulsarMessageSize uint    // maximum sizer of pulsar messages produced
-	clock                clock.Clock
+	producer                 pulsar.Producer
+	jobRepository            database.JobRepository
+	executorRepository       database.ExecutorRepository
+	legacyExecutorRepository database.ExecutorRepository
+	allowedPriorities        []int32 // allowed priority classes
+	maxJobsPerCall           uint    // maximum number of jobs that will be leased in a single call
+	maxPulsarMessageSize     uint    // maximum sizer of pulsar messages produced
+	clock                    clock.Clock
 }
 
 func NewExecutorApi(producer pulsar.Producer,
 	jobRepository database.JobRepository,
 	executorRepository database.ExecutorRepository,
+	legacyExecutorRepository database.ExecutorRepository,
 	allowedPriorities []int32,
 	maxJobsPerCall uint,
 ) (*ExecutorApi, error) {
@@ -48,13 +52,14 @@ func NewExecutorApi(producer pulsar.Producer,
 	}
 
 	return &ExecutorApi{
-		producer:             producer,
-		jobRepository:        jobRepository,
-		executorRepository:   executorRepository,
-		allowedPriorities:    allowedPriorities,
-		maxJobsPerCall:       maxJobsPerCall,
-		maxPulsarMessageSize: 1024 * 1024 * 2,
-		clock:                clock.RealClock{},
+		producer:                 producer,
+		jobRepository:            jobRepository,
+		executorRepository:       executorRepository,
+		legacyExecutorRepository: legacyExecutorRepository,
+		allowedPriorities:        allowedPriorities,
+		maxJobsPerCall:           maxJobsPerCall,
+		maxPulsarMessageSize:     1024 * 1024 * 2,
+		clock:                    clock.RealClock{},
 	}, nil
 }
 
@@ -79,6 +84,11 @@ func (srv *ExecutorApi) LeaseJobRuns(stream executorapi.ExecutorApi_LeaseJobRuns
 		return err
 	}
 
+	// store the executor state  for the legacy executor to use
+	if err = srv.legacyExecutorRepository.StoreExecutor(stream.Context(), executorState); err != nil {
+		return err
+	}
+
 	requestRuns, err := extractRunIds(req)
 	if err != nil {
 		return err
@@ -161,7 +171,7 @@ func (srv *ExecutorApi) LeaseJobRuns(stream executorapi.ExecutorApi_LeaseJobRuns
 
 // ReportEvents publishes all events to pulsar. The events are compacted for more efficient publishing
 func (srv *ExecutorApi) ReportEvents(ctx context.Context, list *executorapi.EventList) (*types.Empty, error) {
-	err := pulsarutils.CompactAndPublishSequences(ctx, list.Events, srv.producer, srv.maxPulsarMessageSize)
+	err := pulsarutils.CompactAndPublishSequences(ctx, list.Events, srv.producer, srv.maxPulsarMessageSize, schedulers.Pulsar)
 	return &types.Empty{}, err
 }
 
diff --git a/internal/scheduler/database/redis_executor_repository.go b/internal/scheduler/database/redis_executor_repository.go
new file mode 100644
index 00000000..989710a6
--- /dev/null
+++ b/internal/scheduler/database/redis_executor_repository.go
@@ -0,0 +1,68 @@
+package database
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/go-redis/redis"
+	"github.com/gogo/protobuf/proto"
+	"github.com/pkg/errors"
+
+	protoutil "github.com/armadaproject/armada/internal/common/proto"
+	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
+)
+
+const (
+	executorsPrefix = "executors"
+)
+
+type RedisExecutorRepository struct {
+	db           redis.UniversalClient
+	executorsKey string
+}
+
+func NewRedisExecutorRepository(db redis.UniversalClient, schedulerName string) *RedisExecutorRepository {
+	return &RedisExecutorRepository{
+		db:           db,
+		executorsKey: fmt.Sprintf("%s_%s", executorsPrefix, schedulerName),
+	}
+}
+
+func (r *RedisExecutorRepository) GetExecutors(_ context.Context) ([]*schedulerobjects.Executor, error) {
+	result, err := r.db.HGetAll(r.executorsKey).Result()
+	if err != nil {
+		return nil, errors.Wrap(err, "Error retrieving executors from redis")
+	}
+	executors := make([]*schedulerobjects.Executor, len(result))
+	i := 0
+	for _, v := range result {
+		executor, err := protoutil.Unmarshall([]byte(v), &schedulerobjects.Executor{})
+		if err != nil {
+			return nil, err
+		}
+		executors[i] = executor
+		i++
+	}
+	return executors, nil
+}
+
+func (r *RedisExecutorRepository) GetLastUpdateTimes(_ context.Context) (map[string]time.Time, error) {
+	// We could implement this in a very inefficient way, but I don't believe it's needed so panic for now
+	panic("GetLastUpdateTimes is not implemented")
+}
+
+func (r *RedisExecutorRepository) StoreExecutor(_ context.Context, executor *schedulerobjects.Executor) error {
+	data, err := proto.Marshal(executor)
+	if err != nil {
+		return errors.Wrap(err, "Error marshalling executor proto")
+	}
+
+	pipe := r.db.TxPipeline()
+	pipe.HSet(r.executorsKey, executor.Id, data)
+	_, err = pipe.Exec()
+	if err != nil {
+		return errors.Wrap(err, "Error storing executor in redis")
+	}
+	return nil
+}
diff --git a/internal/scheduler/legacyscheduler.go b/internal/scheduler/legacyscheduler.go
index b41df47e..fee90070 100644
--- a/internal/scheduler/legacyscheduler.go
+++ b/internal/scheduler/legacyscheduler.go
@@ -960,21 +960,25 @@ func ResourceListAsWeightedApproximateFloat64(resourceScarcity map[string]float6
 func PodRequirementsFromLegacySchedulerJobs[S ~[]E, E LegacySchedulerJob](jobs S, priorityClasses map[string]configuration.PriorityClass) []*schedulerobjects.PodRequirements {
 	rv := make([]*schedulerobjects.PodRequirements, 0, len(jobs))
 	for _, job := range jobs {
-		info := job.GetRequirements(priorityClasses)
-		req := PodRequirementFromJobSchedulingInfo(info)
-		if _, ok := req.Annotations[JobIdAnnotation]; !ok {
-			// Auto-populate JobIdAnnotation if not set.
-			if req.Annotations == nil {
-				req.Annotations = map[string]string{
-					JobIdAnnotation: job.GetId(),
-				}
-			} else {
-				req.Annotations[JobIdAnnotation] = job.GetId()
+		rv = append(rv, PodRequirementsFromLegacySchedulerJob(job, priorityClasses))
+	}
+	return rv
+}
+
+func PodRequirementsFromLegacySchedulerJob(job LegacySchedulerJob, priorityClasses map[string]configuration.PriorityClass) *schedulerobjects.PodRequirements {
+	info := job.GetRequirements(priorityClasses)
+	req := PodRequirementFromJobSchedulingInfo(info)
+	if _, ok := req.Annotations[JobIdAnnotation]; !ok {
+		// Auto-populate JobIdAnnotation if not set.
+		if req.Annotations == nil {
+			req.Annotations = map[string]string{
+				JobIdAnnotation: job.GetId(),
 			}
+		} else {
+			req.Annotations[JobIdAnnotation] = job.GetId()
 		}
-		rv = append(rv, req)
 	}
-	return rv
+	return req
 }
 
 func PodRequirementsFromJobSchedulingInfos(infos []*schedulerobjects.JobSchedulingInfo) []*schedulerobjects.PodRequirements {
diff --git a/internal/scheduler/mocks/generate.go b/internal/scheduler/mocks/generate.go
index 36b9bc2c..3e310b31 100644
--- a/internal/scheduler/mocks/generate.go
+++ b/internal/scheduler/mocks/generate.go
@@ -1,7 +1,6 @@
 package schedulermocks
 
 // Mock implementations used by scheduler tests
-//go:generate mockgen -destination=./mock_pulsar.go -package=schedulermocks "github.com/apache/pulsar-client-go/pulsar" Client,Producer
 //go:generate mockgen -destination=./mock_leases_getter.go -package=schedulermocks "k8s.io/client-go/kubernetes/typed/coordination/v1" LeasesGetter,LeaseInterface
 //go:generate mockgen -destination=./mock_repositories.go -package=schedulermocks "github.com/armadaproject/armada/internal/scheduler/database" ExecutorRepository,QueueRepository,JobRepository
 //go:generate mockgen -destination=./mock_grpc.go -package=schedulermocks "github.com/armadaproject/armada/pkg/executorapi" ExecutorApi_LeaseJobRunsServer
diff --git a/internal/scheduler/mocks/mock_pulsar.go b/internal/scheduler/mocks/mock_pulsar.go
deleted file mode 100644
index 6b423834..00000000
--- a/internal/scheduler/mocks/mock_pulsar.go
+++ /dev/null
@@ -1,241 +0,0 @@
-// Code generated by MockGen. DO NOT EDIT.
-// Source: github.com/apache/pulsar-client-go/pulsar (interfaces: Client,Producer)
-
-// Package schedulermocks is a generated GoMock package.
-package schedulermocks
-
-import (
-	context "context"
-	reflect "reflect"
-
-	pulsar "github.com/apache/pulsar-client-go/pulsar"
-	gomock "github.com/golang/mock/gomock"
-)
-
-// MockClient is a mock of Client interface.
-type MockClient struct {
-	ctrl     *gomock.Controller
-	recorder *MockClientMockRecorder
-}
-
-// MockClientMockRecorder is the mock recorder for MockClient.
-type MockClientMockRecorder struct {
-	mock *MockClient
-}
-
-// NewMockClient creates a new mock instance.
-func NewMockClient(ctrl *gomock.Controller) *MockClient {
-	mock := &MockClient{ctrl: ctrl}
-	mock.recorder = &MockClientMockRecorder{mock}
-	return mock
-}
-
-// EXPECT returns an object that allows the caller to indicate expected use.
-func (m *MockClient) EXPECT() *MockClientMockRecorder {
-	return m.recorder
-}
-
-// Close mocks base method.
-func (m *MockClient) Close() {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "Close")
-}
-
-// Close indicates an expected call of Close.
-func (mr *MockClientMockRecorder) Close() *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockClient)(nil).Close))
-}
-
-// CreateProducer mocks base method.
-func (m *MockClient) CreateProducer(arg0 pulsar.ProducerOptions) (pulsar.Producer, error) {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "CreateProducer", arg0)
-	ret0, _ := ret[0].(pulsar.Producer)
-	ret1, _ := ret[1].(error)
-	return ret0, ret1
-}
-
-// CreateProducer indicates an expected call of CreateProducer.
-func (mr *MockClientMockRecorder) CreateProducer(arg0 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateProducer", reflect.TypeOf((*MockClient)(nil).CreateProducer), arg0)
-}
-
-// CreateReader mocks base method.
-func (m *MockClient) CreateReader(arg0 pulsar.ReaderOptions) (pulsar.Reader, error) {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "CreateReader", arg0)
-	ret0, _ := ret[0].(pulsar.Reader)
-	ret1, _ := ret[1].(error)
-	return ret0, ret1
-}
-
-// CreateReader indicates an expected call of CreateReader.
-func (mr *MockClientMockRecorder) CreateReader(arg0 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateReader", reflect.TypeOf((*MockClient)(nil).CreateReader), arg0)
-}
-
-// CreateTableView mocks base method.
-func (m *MockClient) CreateTableView(arg0 pulsar.TableViewOptions) (pulsar.TableView, error) {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "CreateTableView", arg0)
-	ret0, _ := ret[0].(pulsar.TableView)
-	ret1, _ := ret[1].(error)
-	return ret0, ret1
-}
-
-// CreateTableView indicates an expected call of CreateTableView.
-func (mr *MockClientMockRecorder) CreateTableView(arg0 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTableView", reflect.TypeOf((*MockClient)(nil).CreateTableView), arg0)
-}
-
-// Subscribe mocks base method.
-func (m *MockClient) Subscribe(arg0 pulsar.ConsumerOptions) (pulsar.Consumer, error) {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Subscribe", arg0)
-	ret0, _ := ret[0].(pulsar.Consumer)
-	ret1, _ := ret[1].(error)
-	return ret0, ret1
-}
-
-// Subscribe indicates an expected call of Subscribe.
-func (mr *MockClientMockRecorder) Subscribe(arg0 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subscribe", reflect.TypeOf((*MockClient)(nil).Subscribe), arg0)
-}
-
-// TopicPartitions mocks base method.
-func (m *MockClient) TopicPartitions(arg0 string) ([]string, error) {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "TopicPartitions", arg0)
-	ret0, _ := ret[0].([]string)
-	ret1, _ := ret[1].(error)
-	return ret0, ret1
-}
-
-// TopicPartitions indicates an expected call of TopicPartitions.
-func (mr *MockClientMockRecorder) TopicPartitions(arg0 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TopicPartitions", reflect.TypeOf((*MockClient)(nil).TopicPartitions), arg0)
-}
-
-// MockProducer is a mock of Producer interface.
-type MockProducer struct {
-	ctrl     *gomock.Controller
-	recorder *MockProducerMockRecorder
-}
-
-// MockProducerMockRecorder is the mock recorder for MockProducer.
-type MockProducerMockRecorder struct {
-	mock *MockProducer
-}
-
-// NewMockProducer creates a new mock instance.
-func NewMockProducer(ctrl *gomock.Controller) *MockProducer {
-	mock := &MockProducer{ctrl: ctrl}
-	mock.recorder = &MockProducerMockRecorder{mock}
-	return mock
-}
-
-// EXPECT returns an object that allows the caller to indicate expected use.
-func (m *MockProducer) EXPECT() *MockProducerMockRecorder {
-	return m.recorder
-}
-
-// Close mocks base method.
-func (m *MockProducer) Close() {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "Close")
-}
-
-// Close indicates an expected call of Close.
-func (mr *MockProducerMockRecorder) Close() *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockProducer)(nil).Close))
-}
-
-// Flush mocks base method.
-func (m *MockProducer) Flush() error {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Flush")
-	ret0, _ := ret[0].(error)
-	return ret0
-}
-
-// Flush indicates an expected call of Flush.
-func (mr *MockProducerMockRecorder) Flush() *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Flush", reflect.TypeOf((*MockProducer)(nil).Flush))
-}
-
-// LastSequenceID mocks base method.
-func (m *MockProducer) LastSequenceID() int64 {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "LastSequenceID")
-	ret0, _ := ret[0].(int64)
-	return ret0
-}
-
-// LastSequenceID indicates an expected call of LastSequenceID.
-func (mr *MockProducerMockRecorder) LastSequenceID() *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LastSequenceID", reflect.TypeOf((*MockProducer)(nil).LastSequenceID))
-}
-
-// Name mocks base method.
-func (m *MockProducer) Name() string {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Name")
-	ret0, _ := ret[0].(string)
-	return ret0
-}
-
-// Name indicates an expected call of Name.
-func (mr *MockProducerMockRecorder) Name() *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockProducer)(nil).Name))
-}
-
-// Send mocks base method.
-func (m *MockProducer) Send(arg0 context.Context, arg1 *pulsar.ProducerMessage) (pulsar.MessageID, error) {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Send", arg0, arg1)
-	ret0, _ := ret[0].(pulsar.MessageID)
-	ret1, _ := ret[1].(error)
-	return ret0, ret1
-}
-
-// Send indicates an expected call of Send.
-func (mr *MockProducerMockRecorder) Send(arg0, arg1 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Send", reflect.TypeOf((*MockProducer)(nil).Send), arg0, arg1)
-}
-
-// SendAsync mocks base method.
-func (m *MockProducer) SendAsync(arg0 context.Context, arg1 *pulsar.ProducerMessage, arg2 func(pulsar.MessageID, *pulsar.ProducerMessage, error)) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "SendAsync", arg0, arg1, arg2)
-}
-
-// SendAsync indicates an expected call of SendAsync.
-func (mr *MockProducerMockRecorder) SendAsync(arg0, arg1, arg2 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendAsync", reflect.TypeOf((*MockProducer)(nil).SendAsync), arg0, arg1, arg2)
-}
-
-// Topic mocks base method.
-func (m *MockProducer) Topic() string {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Topic")
-	ret0, _ := ret[0].(string)
-	return ret0
-}
-
-// Topic indicates an expected call of Topic.
-func (mr *MockProducerMockRecorder) Topic() *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Topic", reflect.TypeOf((*MockProducer)(nil).Topic))
-}
diff --git a/internal/scheduler/nodedb.go b/internal/scheduler/nodedb.go
index db8a2567..be43f2d8 100644
--- a/internal/scheduler/nodedb.go
+++ b/internal/scheduler/nodedb.go
@@ -25,8 +25,6 @@ type NodeDb struct {
 	// In-memory database. Stores *SchedulerNode.
 	// Used to efficiently iterate over nodes in sorted order.
 	db *memdb.MemDB
-	// Time at which the most recent upsert took place.
-	timeOfMostRecentUpsert time.Time
 	// Allowed pod priorities in sorted order.
 	// Because the number of database indices scales linearly with the number of distinct priorities,
 	// the efficiency of the NodeDb relies on the number of distinct priorities being small.
@@ -424,19 +422,10 @@ func (nodeDb *NodeDb) Upsert(nodes []*schedulerobjects.Node) error {
 			return errors.WithStack(err)
 		}
 	}
-	nodeDb.mu.Lock()
-	nodeDb.timeOfMostRecentUpsert = time.Now()
-	nodeDb.mu.Unlock()
 	txn.Commit()
 	return nil
 }
 
-func (nodeDb *NodeDb) TimeOfMostRecentUpsert() time.Time {
-	nodeDb.mu.Lock()
-	defer nodeDb.mu.Unlock()
-	return nodeDb.timeOfMostRecentUpsert
-}
-
 // ClearAllocated zeroes out allocated resources on all nodes in the NodeDb.
 func (nodeDb *NodeDb) ClearAllocated() error {
 	txn := nodeDb.db.Txn(true)
diff --git a/internal/scheduler/publisher.go b/internal/scheduler/publisher.go
index 6cf73a54..d33d2434 100644
--- a/internal/scheduler/publisher.go
+++ b/internal/scheduler/publisher.go
@@ -14,6 +14,7 @@ import (
 	log "github.com/sirupsen/logrus"
 
 	"github.com/armadaproject/armada/internal/common/eventutil"
+	"github.com/armadaproject/armada/internal/common/schedulers"
 	"github.com/armadaproject/armada/pkg/armadaevents"
 )
 
@@ -82,10 +83,10 @@ func (p *PulsarPublisher) PublishMessages(ctx context.Context, events []*armadae
 		}
 		msgs[i] = &pulsar.ProducerMessage{
 			Payload: bytes,
+			Key:     sequences[i].JobSetName,
 			Properties: map[string]string{
-				armadaevents.PULSAR_MESSAGE_TYPE_PROPERTY: armadaevents.PULSAR_CONTROL_MESSAGE,
+				schedulers.PropertyName: schedulers.PulsarSchedulerAttribute,
 			},
-			Key: sequences[i].JobSetName,
 		}
 	}
 
@@ -143,8 +144,8 @@ func (p *PulsarPublisher) PublishMarkers(ctx context.Context, groupId uuid.UUID)
 		}
 		msg := &pulsar.ProducerMessage{
 			Properties: map[string]string{
-				armadaevents.PULSAR_MESSAGE_TYPE_PROPERTY: armadaevents.PULSAR_CONTROL_MESSAGE,
-				explicitPartitionKey:                      fmt.Sprintf("%d", i),
+				explicitPartitionKey:    fmt.Sprintf("%d", i),
+				schedulers.PropertyName: schedulers.PulsarSchedulerAttribute,
 			},
 			Payload: bytes,
 		}
diff --git a/internal/scheduler/schedulerapp.go b/internal/scheduler/schedulerapp.go
index ead41cab..ab9c7be3 100644
--- a/internal/scheduler/schedulerapp.go
+++ b/internal/scheduler/schedulerapp.go
@@ -15,7 +15,6 @@ import (
 	"k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/rest"
 
-	"github.com/armadaproject/armada/internal/armada/configuration"
 	"github.com/armadaproject/armada/internal/common/app"
 	"github.com/armadaproject/armada/internal/common/auth"
 	dbcommon "github.com/armadaproject/armada/internal/common/database"
@@ -55,6 +54,7 @@ func Run(config Configuration) error {
 		}
 	}()
 	queueRepository := database.NewLegacyQueueRepository(redisClient)
+	legacyExecutorRepository := database.NewRedisExecutorRepository(redisClient, "pulsar")
 
 	//////////////////////////////////////////////////////////////////////////
 	// Pulsar
@@ -110,8 +110,14 @@ func Run(config Configuration) error {
 	if err != nil {
 		return errors.WithMessage(err, "error setting up grpc server")
 	}
-	allowedPcs := allowedPrioritiesFromPriorityClasses(config.Scheduling.Preemption.PriorityClasses)
-	executorServer, err := NewExecutorApi(apiProducer, jobRepository, executorRepository, allowedPcs, config.Scheduling.MaximumJobsToSchedule)
+	allowedPcs := config.Scheduling.Preemption.AllowedPriorities()
+	executorServer, err := NewExecutorApi(
+		apiProducer,
+		jobRepository,
+		executorRepository,
+		legacyExecutorRepository,
+		allowedPcs,
+		config.Scheduling.MaximumJobsToSchedule)
 	if err != nil {
 		return errors.WithMessage(err, "error creating executorApi")
 	}
@@ -173,11 +179,3 @@ func createLeaderController(config LeaderConfig) (LeaderController, error) {
 		return nil, errors.Errorf("%s is not a value leader mode", config.Mode)
 	}
 }
-
-func allowedPrioritiesFromPriorityClasses(pcs map[string]configuration.PriorityClass) []int32 {
-	allowedPcs := make([]int32, 0, len(pcs))
-	for _, v := range pcs {
-		allowedPcs = append(allowedPcs, v.Priority)
-	}
-	return allowedPcs
-}
diff --git a/internal/scheduler/schedulerobjects/schedulerobjects.pb.go b/internal/scheduler/schedulerobjects/schedulerobjects.pb.go
index df8440b4..8abcbc91 100644
--- a/internal/scheduler/schedulerobjects/schedulerobjects.pb.go
+++ b/internal/scheduler/schedulerobjects/schedulerobjects.pb.go
@@ -764,6 +764,68 @@ func (m *PodRequirements) GetResourceRequirements() v1.ResourceRequirements {
 	return v1.ResourceRequirements{}
 }
 
+// Used to store details about pulsar scheduler jobs in Redis
+// Can be removed once we deprecate the legacy scheduler
+type PulsarSchedulerJobDetails struct {
+	JobId  string `protobuf:"bytes,1,opt,name=JobId,proto3" json:"JobId,omitempty"`
+	Queue  string `protobuf:"bytes,2,opt,name=Queue,proto3" json:"Queue,omitempty"`
+	JobSet string `protobuf:"bytes,3,opt,name=JobSet,proto3" json:"JobSet,omitempty"`
+}
+
+func (m *PulsarSchedulerJobDetails) Reset()         { *m = PulsarSchedulerJobDetails{} }
+func (m *PulsarSchedulerJobDetails) String() string { return proto.CompactTextString(m) }
+func (*PulsarSchedulerJobDetails) ProtoMessage()    {}
+func (*PulsarSchedulerJobDetails) Descriptor() ([]byte, []int) {
+	return fileDescriptor_97dadc5fbd620721, []int{9}
+}
+func (m *PulsarSchedulerJobDetails) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *PulsarSchedulerJobDetails) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	if deterministic {
+		return xxx_messageInfo_PulsarSchedulerJobDetails.Marshal(b, m, deterministic)
+	} else {
+		b = b[:cap(b)]
+		n, err := m.MarshalToSizedBuffer(b)
+		if err != nil {
+			return nil, err
+		}
+		return b[:n], nil
+	}
+}
+func (m *PulsarSchedulerJobDetails) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_PulsarSchedulerJobDetails.Merge(m, src)
+}
+func (m *PulsarSchedulerJobDetails) XXX_Size() int {
+	return m.Size()
+}
+func (m *PulsarSchedulerJobDetails) XXX_DiscardUnknown() {
+	xxx_messageInfo_PulsarSchedulerJobDetails.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_PulsarSchedulerJobDetails proto.InternalMessageInfo
+
+func (m *PulsarSchedulerJobDetails) GetJobId() string {
+	if m != nil {
+		return m.JobId
+	}
+	return ""
+}
+
+func (m *PulsarSchedulerJobDetails) GetQueue() string {
+	if m != nil {
+		return m.Queue
+	}
+	return ""
+}
+
+func (m *PulsarSchedulerJobDetails) GetJobSet() string {
+	if m != nil {
+		return m.JobSet
+	}
+	return ""
+}
+
 func init() {
 	proto.RegisterType((*Executor)(nil), "schedulerobjects.Executor")
 	proto.RegisterType((*Node)(nil), "schedulerobjects.Node")
@@ -784,6 +846,7 @@ func init() {
 	proto.RegisterType((*PodRequirements)(nil), "schedulerobjects.PodRequirements")
 	proto.RegisterMapType((map[string]string)(nil), "schedulerobjects.PodRequirements.AnnotationsEntry")
 	proto.RegisterMapType((map[string]string)(nil), "schedulerobjects.PodRequirements.NodeSelectorEntry")
+	proto.RegisterType((*PulsarSchedulerJobDetails)(nil), "schedulerobjects.PulsarSchedulerJobDetails")
 }
 
 func init() {
@@ -791,104 +854,108 @@ func init() {
 }
 
 var fileDescriptor_97dadc5fbd620721 = []byte{
-	// 1543 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x58, 0xcf, 0x6f, 0xdc, 0xc4,
-	0x17, 0x8f, 0x77, 0xf3, 0x63, 0x33, 0x49, 0xf3, 0x63, 0xf2, 0xe3, 0xeb, 0x6c, 0xfb, 0x5d, 0x87,
-	0x05, 0xaa, 0x20, 0x5a, 0x2f, 0x4d, 0x39, 0x54, 0x85, 0x4b, 0x5c, 0x2a, 0xd8, 0x28, 0xb4, 0xa9,
-	0x93, 0x20, 0x81, 0x44, 0x2b, 0xaf, 0x3d, 0xd9, 0x4e, 0xe3, 0xf5, 0xb8, 0xf6, 0x38, 0xea, 0xf6,
-	0x0c, 0x27, 0xa4, 0xaa, 0x02, 0x0e, 0x48, 0x48, 0x9c, 0x39, 0x73, 0xe5, 0x1f, 0xe8, 0xb1, 0x47,
-	0x4e, 0x2e, 0x6a, 0x6f, 0x2b, 0x71, 0xe1, 0x1f, 0x00, 0x79, 0xc6, 0x5e, 0xcf, 0xda, 0xde, 0x6c,
-	0x0a, 0x14, 0x4e, 0xc9, 0xbc, 0x79, 0xef, 0xf3, 0x7e, 0xce, 0x7b, 0xcf, 0x0b, 0xae, 0x62, 0x87,
-	0x22, 0xcf, 0x31, 0xec, 0x86, 0x6f, 0xde, 0x45, 0x56, 0x60, 0x23, 0x2f, 0xfd, 0x8f, 0xb4, 0xee,
-	0x21, 0x93, 0xfa, 0x39, 0x82, 0xea, 0x7a, 0x84, 0x12, 0xb8, 0x90, 0xa5, 0x57, 0x95, 0x36, 0x21,
-	0x6d, 0x1b, 0x35, 0xd8, 0x7d, 0x2b, 0x38, 0x6c, 0x50, 0xdc, 0x41, 0x3e, 0x35, 0x3a, 0x2e, 0x17,
-	0xa9, 0xd6, 0x8f, 0xae, 0xf8, 0x2a, 0x26, 0x0d, 0xc3, 0xc5, 0x0d, 0x93, 0x78, 0xa8, 0x71, 0x7c,
-	0xa9, 0xd1, 0x46, 0x0e, 0xf2, 0x0c, 0x8a, 0xac, 0x98, 0xe7, 0xdd, 0x94, 0xa7, 0x63, 0x98, 0x77,
-	0xb1, 0x83, 0xbc, 0x6e, 0xc3, 0x3d, 0x6a, 0x33, 0x21, 0x0f, 0xf9, 0x24, 0xf0, 0x4c, 0x94, 0x93,
-	0xba, 0xd8, 0xc6, 0xf4, 0x6e, 0xd0, 0x52, 0x4d, 0xd2, 0x69, 0xb4, 0x49, 0x9b, 0xa4, 0x36, 0x44,
-	0x27, 0x76, 0x60, 0xff, 0x71, 0xf6, 0xfa, 0x8f, 0x65, 0x50, 0xb9, 0xfe, 0x00, 0x99, 0x01, 0x25,
-	0x1e, 0x5c, 0x07, 0x25, 0x6c, 0xc9, 0xd2, 0xba, 0xb4, 0x31, 0xad, 0x2d, 0xf4, 0x42, 0x65, 0x16,
-	0x5b, 0x17, 0x48, 0x07, 0x53, 0xd4, 0x71, 0x69, 0x57, 0x2f, 0x61, 0x0b, 0x9e, 0x07, 0xe3, 0x2e,
-	0x21, 0xb6, 0x5c, 0x62, 0x3c, 0xb0, 0x17, 0x2a, 0x73, 0xd1, 0x59, 0xe0, 0x62, 0xf7, 0x70, 0x0b,
-	0x4c, 0x38, 0xc4, 0x42, 0xbe, 0x5c, 0x5e, 0x2f, 0x6f, 0xcc, 0x6c, 0xae, 0xaa, 0xb9, 0xd0, 0xdd,
-	0x20, 0x16, 0xd2, 0x96, 0x7a, 0xa1, 0x32, 0xcf, 0x18, 0x05, 0x04, 0x2e, 0x09, 0x6f, 0x83, 0xb9,
-	0x0e, 0x76, 0x70, 0x27, 0xe8, 0x6c, 0x93, 0xd6, 0x1e, 0x7e, 0x88, 0xe4, 0xf1, 0x75, 0x69, 0x63,
-	0x66, 0xb3, 0x96, 0xc7, 0xd2, 0xe3, 0x60, 0xec, 0x60, 0x9f, 0x6a, 0xab, 0x4f, 0x42, 0x65, 0x2c,
-	0x32, 0x6c, 0x50, 0x5a, 0xcf, 0x9c, 0x23, 0x7c, 0xdb, 0xf0, 0xe9, 0x81, 0x6b, 0x19, 0x14, 0xed,
-	0xe3, 0x0e, 0x92, 0x27, 0x18, 0x7e, 0x55, 0xe5, 0xc9, 0x53, 0x93, 0xc0, 0xa9, 0xfb, 0x49, 0xf2,
-	0xb4, 0x6a, 0x82, 0x3d, 0x28, 0xf9, 0xf8, 0x99, 0x22, 0xe9, 0x19, 0x1a, 0xbc, 0x09, 0x96, 0x02,
-	0xc7, 0xf0, 0x7d, 0xdc, 0x76, 0x90, 0x75, 0xe7, 0x1e, 0x69, 0xdd, 0xf1, 0x02, 0xc7, 0x97, 0xa7,
-	0xd7, 0xcb, 0x1b, 0xd3, 0x9a, 0xd2, 0x0b, 0x95, 0xb3, 0xe9, 0xf5, 0x36, 0x69, 0xe9, 0x81, 0x23,
-	0x06, 0x61, 0x31, 0x77, 0x59, 0x7f, 0x36, 0x0d, 0xc6, 0xa3, 0xa8, 0x45, 0x69, 0x6a, 0x0e, 0xa4,
-	0xa9, 0x39, 0x90, 0xa6, 0xa6, 0x05, 0x77, 0x40, 0x65, 0xc7, 0xf0, 0xe9, 0x1e, 0x42, 0x0e, 0x4b,
-	0xd5, 0xc9, 0x5e, 0x2d, 0xc7, 0x5e, 0xf5, 0x65, 0x98, 0x3f, 0xfd, 0x13, 0xbc, 0x01, 0x2a, 0x51,
-	0x4a, 0xf6, 0xbb, 0x2e, 0x92, 0xcb, 0x31, 0x5a, 0x61, 0x3e, 0x23, 0x0e, 0x6d, 0xb5, 0x17, 0x2a,
-	0x30, 0xe1, 0x17, 0xec, 0xea, 0x63, 0xc0, 0x2b, 0x00, 0x24, 0xdc, 0x4d, 0x8b, 0x65, 0x75, 0x5a,
-	0x93, 0x7b, 0xa1, 0xb2, 0x9c, 0x52, 0x05, 0x39, 0x81, 0x17, 0x6e, 0x81, 0x49, 0x6a, 0x60, 0x87,
-	0xfa, 0xf2, 0x04, 0xab, 0xab, 0x35, 0x95, 0xbf, 0x11, 0xd5, 0x70, 0xb1, 0x1a, 0xbd, 0x23, 0xf5,
-	0xf8, 0x92, 0xba, 0x1f, 0x71, 0x68, 0x73, 0xb1, 0x53, 0xb1, 0x80, 0x1e, 0xff, 0x85, 0xbb, 0x60,
-	0xd2, 0x36, 0x5a, 0xc8, 0xf6, 0xe5, 0x49, 0x06, 0x51, 0x2f, 0x76, 0x45, 0xdd, 0x61, 0x4c, 0xd7,
-	0x1d, 0xea, 0x75, 0xb5, 0xe5, 0x5e, 0xa8, 0x2c, 0x70, 0x29, 0xc1, 0xb0, 0x18, 0x27, 0x2a, 0xa4,
-	0x7d, 0x42, 0x0d, 0x3b, 0xa9, 0x42, 0x5f, 0x9e, 0x7a, 0xb9, 0x42, 0x1d, 0x94, 0xd6, 0x33, 0x67,
-	0xf8, 0x93, 0x04, 0xd6, 0xb7, 0x6c, 0x9b, 0x98, 0x06, 0x35, 0x5a, 0x36, 0xd2, 0xba, 0xbb, 0x1e,
-	0x26, 0x1e, 0xa6, 0xdd, 0x2d, 0xc7, 0x4a, 0xb8, 0xe4, 0x0a, 0x73, 0xe6, 0xfd, 0x21, 0xce, 0x8c,
-	0x12, 0xe7, 0x6e, 0x6e, 0xc4, 0x06, 0x8d, 0xd4, 0xa6, 0x8f, 0xe4, 0x80, 0xef, 0x80, 0x4a, 0xa6,
-	0xe4, 0x57, 0x7a, 0xa1, 0xb2, 0x78, 0x2f, 0x57, 0xe8, 0x53, 0x31, 0x09, 0x7a, 0x60, 0x21, 0x46,
-	0x45, 0x96, 0xd6, 0xdd, 0x26, 0xad, 0xa6, 0x25, 0x03, 0xe6, 0xd5, 0x85, 0x93, 0xbd, 0xea, 0xb3,
-	0x73, 0x2f, 0xe4, 0xd8, 0x8b, 0x1c, 0x9a, 0x9e, 0xa3, 0x54, 0x0d, 0x30, 0x23, 0xe4, 0x19, 0xbe,
-	0x0e, 0xca, 0x47, 0xa8, 0x1b, 0xbf, 0xac, 0xc5, 0x5e, 0xa8, 0x9c, 0x39, 0x42, 0x5d, 0xc1, 0xd6,
-	0xe8, 0x16, 0xbe, 0x05, 0x26, 0x8e, 0x0d, 0x3b, 0x40, 0x71, 0x0f, 0x64, 0x2d, 0x8c, 0x11, 0xc4,
-	0x16, 0xc6, 0x08, 0x57, 0x4b, 0x57, 0xa4, 0xea, 0xf7, 0x12, 0x78, 0xf3, 0x54, 0xe1, 0x17, 0xb5,
-	0x4f, 0x0c, 0xd5, 0xde, 0x14, 0xb5, 0x8f, 0xae, 0xb1, 0x51, 0xd6, 0x7d, 0x25, 0x81, 0x95, 0xc2,
-	0x30, 0x9e, 0x2e, 0x16, 0xff, 0xac, 0x35, 0xf5, 0x1f, 0xc6, 0x41, 0x25, 0x79, 0xed, 0xa7, 0xe8,
-	0x72, 0x69, 0x37, 0x28, 0xfd, 0xd5, 0x6e, 0xb0, 0xdf, 0xef, 0x06, 0x7c, 0x50, 0x9d, 0x1f, 0xde,
-	0xd8, 0x5e, 0xa2, 0x23, 0x7c, 0x21, 0x01, 0x18, 0x38, 0x3e, 0xa2, 0x4d, 0xc7, 0x42, 0x0f, 0x90,
-	0xc5, 0x25, 0xe5, 0x71, 0xa6, 0x62, 0xf3, 0x04, 0x15, 0x07, 0x39, 0x21, 0xae, 0x6e, 0xbd, 0x17,
-	0x2a, 0xe7, 0xf2, 0x88, 0x82, 0xea, 0x02, 0x7d, 0xff, 0x46, 0x75, 0x77, 0xc0, 0xff, 0x86, 0xd8,
-	0xfc, 0x2a, 0xd4, 0xd5, 0x7f, 0x2f, 0x83, 0xb5, 0x5b, 0x01, 0x0a, 0xd0, 0x35, 0x3b, 0xf0, 0x29,
-	0xf2, 0x92, 0xea, 0x3a, 0xf0, 0x8d, 0x36, 0x82, 0x4d, 0x30, 0x65, 0x7a, 0x28, 0xaa, 0x64, 0xa6,
-	0xf5, 0xe4, 0xa1, 0xb7, 0x14, 0x57, 0x44, 0x22, 0xc2, 0x66, 0x5e, 0x72, 0x88, 0xec, 0xba, 0x1f,
-	0xe9, 0x11, 0xed, 0x62, 0x04, 0xd1, 0x2e, 0x46, 0x88, 0xa6, 0x19, 0x8a, 0x17, 0xa8, 0xa6, 0xc5,
-	0xe6, 0x63, 0x3c, 0xcd, 0x52, 0xaa, 0x38, 0xcd, 0x52, 0x2a, 0xfc, 0x46, 0x02, 0x4b, 0xc9, 0x1e,
-	0xe7, 0xa7, 0x8d, 0x21, 0xae, 0x93, 0x0f, 0xf2, 0x75, 0x32, 0xd4, 0xf5, 0xfe, 0x33, 0x13, 0x60,
-	0x78, 0xe5, 0x9c, 0x8d, 0xdd, 0x2c, 0x52, 0xa4, 0x17, 0x11, 0xab, 0x8f, 0x24, 0x20, 0x0f, 0x83,
-	0xfb, 0x2f, 0x7a, 0x54, 0xfd, 0xe7, 0x32, 0xa8, 0x16, 0x39, 0xad, 0x23, 0x97, 0x78, 0xb4, 0xbf,
-	0x92, 0x4a, 0x23, 0x56, 0x52, 0xa1, 0x3a, 0x4a, 0x7f, 0xb3, 0x3a, 0x1e, 0x49, 0x60, 0x41, 0x08,
-	0x1d, 0x4b, 0x4b, 0xdc, 0x40, 0xb4, 0xbc, 0xb3, 0xc3, 0x6d, 0x17, 0xd3, 0xc6, 0x40, 0x78, 0xce,
-	0x6a, 0xbd, 0x50, 0xa9, 0x66, 0xf1, 0x05, 0x7f, 0x72, 0xba, 0xab, 0xdf, 0x49, 0x60, 0xa5, 0x10,
-	0xeb, 0x74, 0xaf, 0xf0, 0x93, 0xc1, 0x84, 0xbd, 0xfd, 0x12, 0x95, 0x37, 0x32, 0x7b, 0x5f, 0x96,
-	0xc0, 0xac, 0x98, 0x6e, 0x78, 0x1b, 0x4c, 0xf7, 0xed, 0x97, 0x25, 0x16, 0xb4, 0x8b, 0x27, 0x57,
-	0x48, 0x1a, 0x26, 0x1e, 0x9f, 0xc5, 0x38, 0x39, 0x29, 0x8e, 0x9e, 0xfe, 0x5b, 0xfd, 0x56, 0x02,
-	0x73, 0x83, 0x02, 0xa7, 0x0b, 0xc2, 0xa7, 0x83, 0x41, 0x50, 0x85, 0x61, 0xd2, 0xff, 0xfc, 0x52,
-	0xdd, 0xa3, 0x36, 0x9b, 0x2e, 0x89, 0x3a, 0xf5, 0x56, 0x60, 0x38, 0x14, 0xd3, 0xee, 0xc8, 0x38,
-	0xfc, 0x51, 0x02, 0x8b, 0xd1, 0xa7, 0x07, 0x77, 0x14, 0x3b, 0xed, 0xa6, 0x73, 0x48, 0xe0, 0x26,
-	0xa8, 0xd8, 0xf8, 0x10, 0x45, 0x9f, 0x87, 0xcc, 0xbc, 0x33, 0x7c, 0x7d, 0x4e, 0x68, 0xe2, 0xfa,
-	0x9c, 0xd0, 0xa2, 0x86, 0x63, 0xd0, 0x8f, 0x89, 0x4f, 0x6f, 0x3a, 0x26, 0xb7, 0xb6, 0xc2, 0x1b,
-	0x4e, 0x4a, 0x15, 0x1b, 0x4e, 0x4a, 0x85, 0xef, 0x81, 0x19, 0xd7, 0x43, 0x11, 0x1d, 0xb7, 0x6c,
-	0xbe, 0xcb, 0x57, 0xb4, 0xb5, 0x5e, 0xa8, 0xac, 0x08, 0x64, 0x41, 0x56, 0xe4, 0x86, 0x1f, 0x82,
-	0x79, 0x93, 0x38, 0x66, 0xe0, 0x79, 0xc8, 0x31, 0xbb, 0x7b, 0xc6, 0x21, 0xff, 0x20, 0xab, 0x68,
-	0xff, 0xef, 0x85, 0xca, 0x5a, 0xe6, 0x4a, 0x00, 0xc9, 0x4a, 0xc1, 0x63, 0x00, 0x79, 0x96, 0x75,
-	0x74, 0x3f, 0xc0, 0x1e, 0xea, 0xa0, 0x74, 0xa1, 0x7f, 0x23, 0x5f, 0x09, 0x37, 0x73, 0xbc, 0x7c,
-	0x1c, 0xe6, 0x31, 0xc4, 0x71, 0x98, 0xbf, 0xad, 0x7f, 0x2d, 0x01, 0x98, 0x07, 0x83, 0x36, 0x98,
-	0x77, 0x89, 0x35, 0x60, 0x0b, 0x9f, 0x1e, 0xaf, 0xe5, 0x6d, 0xd9, 0x1d, 0x64, 0xe4, 0xae, 0x67,
-	0xa4, 0x53, 0x2b, 0x3e, 0x1a, 0xd3, 0xb3, 0xd0, 0xda, 0x1c, 0x98, 0xf5, 0x44, 0xa3, 0x7e, 0x9b,
-	0x04, 0xf3, 0x19, 0x54, 0xe8, 0x83, 0xd9, 0xe8, 0x5b, 0x69, 0x0f, 0xd9, 0xc8, 0xa4, 0xc4, 0x8b,
-	0x1f, 0xc9, 0xe5, 0x91, 0xe6, 0xb0, 0x3d, 0x22, 0x91, 0xe2, 0x4f, 0xa5, 0xda, 0x0b, 0x95, 0x55,
-	0x11, 0x4c, 0x88, 0xd1, 0x80, 0x12, 0xb8, 0x0b, 0x2a, 0xc6, 0xe1, 0x21, 0x76, 0xa2, 0x01, 0xc4,
-	0x5f, 0xc0, 0xb9, 0xa2, 0x75, 0x6a, 0x2b, 0xe6, 0xe1, 0x75, 0x9a, 0x48, 0x88, 0x75, 0x9a, 0xd0,
-	0xe0, 0x01, 0x98, 0xa1, 0xc4, 0x46, 0x9e, 0x41, 0x31, 0x71, 0x92, 0x05, 0xab, 0x56, 0xb8, 0xa3,
-	0xf5, 0xd9, 0xfa, 0x8d, 0x57, 0x14, 0xd5, 0xc5, 0x03, 0x24, 0x60, 0xc6, 0x70, 0x1c, 0x42, 0x63,
-	0xd8, 0xa9, 0x61, 0x4b, 0x55, 0x36, 0x38, 0x5b, 0xa9, 0x10, 0x8f, 0x0d, 0x2b, 0x7c, 0x01, 0x4a,
-	0x2c, 0x7c, 0x81, 0x1c, 0xbd, 0x51, 0x37, 0x1d, 0xcd, 0xd1, 0xe0, 0x63, 0xbe, 0x27, 0x34, 0xd1,
-	0xf7, 0x84, 0x06, 0xb7, 0xc1, 0x42, 0xf2, 0x76, 0x88, 0xb3, 0x4b, 0x6c, 0x6c, 0x76, 0xd9, 0xcf,
-	0x0b, 0xd3, 0xbc, 0xb9, 0x67, 0xef, 0xc4, 0xe6, 0x9e, 0xbd, 0x83, 0x0f, 0xc1, 0x72, 0xd2, 0x6e,
-	0x06, 0xaa, 0x74, 0x92, 0x65, 0x69, 0xa3, 0x28, 0xa0, 0x7a, 0x01, 0xbf, 0x76, 0x2e, 0x0e, 0x6d,
-	0x21, 0x9a, 0x5e, 0x48, 0xad, 0xb6, 0xc1, 0x62, 0xae, 0xa8, 0x5e, 0xc9, 0x22, 0x79, 0x08, 0x16,
-	0xb2, 0x09, 0x7a, 0x15, 0x7a, 0xb4, 0xcf, 0x9f, 0x3c, 0xaf, 0x49, 0x4f, 0x9f, 0xd7, 0xa4, 0x5f,
-	0x9f, 0xd7, 0xa4, 0xc7, 0x2f, 0x6a, 0x63, 0x4f, 0x5f, 0xd4, 0xc6, 0x7e, 0x79, 0x51, 0x1b, 0xfb,
-	0xec, 0x9a, 0xf0, 0xc3, 0x99, 0xe1, 0x75, 0x0c, 0xcb, 0x70, 0x3d, 0x12, 0x95, 0x52, 0x7c, 0x6a,
-	0x9c, 0xe2, 0x17, 0xc2, 0xd6, 0x24, 0xdb, 0x25, 0x2e, 0xff, 0x19, 0x00, 0x00, 0xff, 0xff, 0xc0,
-	0xa5, 0x66, 0x3e, 0x4f, 0x14, 0x00, 0x00,
+	// 1605 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x58, 0xcd, 0x6f, 0xdb, 0xc6,
+	0x12, 0x37, 0x25, 0x7f, 0xc8, 0x6b, 0xc7, 0x1f, 0xeb, 0x8f, 0x47, 0x2b, 0x79, 0xa2, 0x9f, 0xde,
+	0x7b, 0x81, 0x8b, 0x26, 0x54, 0xe3, 0xf4, 0x10, 0xa4, 0xbd, 0x98, 0x49, 0xd0, 0xca, 0x70, 0x13,
+	0x87, 0xb6, 0x0b, 0xb4, 0x40, 0x13, 0xac, 0xc4, 0xb5, 0xb2, 0x31, 0xc5, 0x65, 0xc8, 0xa5, 0x11,
+	0xe5, 0xdc, 0x9e, 0x0a, 0x04, 0x41, 0xdb, 0x43, 0x81, 0x02, 0xed, 0xb5, 0xe7, 0x5e, 0xfb, 0x0f,
+	0xe4, 0x98, 0x63, 0x4f, 0x4c, 0x91, 0xdc, 0x04, 0xf4, 0xd2, 0x7f, 0xa0, 0x05, 0x77, 0x49, 0x71,
+	0x45, 0x4a, 0x96, 0xd3, 0x36, 0xed, 0xc9, 0xde, 0xd9, 0x99, 0xdf, 0x7c, 0xee, 0xcc, 0x50, 0xe0,
+	0x2a, 0x71, 0x18, 0xf6, 0x1c, 0x64, 0xd7, 0xfc, 0xe6, 0x3d, 0x6c, 0x05, 0x36, 0xf6, 0xd2, 0xff,
+	0x68, 0xe3, 0x3e, 0x6e, 0x32, 0x3f, 0x47, 0xd0, 0x5d, 0x8f, 0x32, 0x0a, 0x17, 0xb2, 0xf4, 0xb2,
+	0xd6, 0xa2, 0xb4, 0x65, 0xe3, 0x1a, 0xbf, 0x6f, 0x04, 0x87, 0x35, 0x46, 0xda, 0xd8, 0x67, 0xa8,
+	0xed, 0x0a, 0x91, 0x72, 0xf5, 0xe8, 0x8a, 0xaf, 0x13, 0x5a, 0x43, 0x2e, 0xa9, 0x35, 0xa9, 0x87,
+	0x6b, 0xc7, 0x97, 0x6a, 0x2d, 0xec, 0x60, 0x0f, 0x31, 0x6c, 0xc5, 0x3c, 0x6f, 0xa7, 0x3c, 0x6d,
+	0xd4, 0xbc, 0x47, 0x1c, 0xec, 0x75, 0x6a, 0xee, 0x51, 0x8b, 0x0b, 0x79, 0xd8, 0xa7, 0x81, 0xd7,
+	0xc4, 0x39, 0xa9, 0x8b, 0x2d, 0xc2, 0xee, 0x05, 0x0d, 0xbd, 0x49, 0xdb, 0xb5, 0x16, 0x6d, 0xd1,
+	0xd4, 0x86, 0xe8, 0xc4, 0x0f, 0xfc, 0x3f, 0xc1, 0x5e, 0xfd, 0xbe, 0x08, 0x4a, 0x37, 0x1e, 0xe2,
+	0x66, 0xc0, 0xa8, 0x07, 0xd7, 0x41, 0x81, 0x58, 0xaa, 0xb2, 0xae, 0x6c, 0x4c, 0x1b, 0x0b, 0xdd,
+	0x50, 0x9b, 0x25, 0xd6, 0x05, 0xda, 0x26, 0x0c, 0xb7, 0x5d, 0xd6, 0x31, 0x0b, 0xc4, 0x82, 0xe7,
+	0xc1, 0xb8, 0x4b, 0xa9, 0xad, 0x16, 0x38, 0x0f, 0xec, 0x86, 0xda, 0x5c, 0x74, 0x96, 0xb8, 0xf8,
+	0x3d, 0xdc, 0x02, 0x13, 0x0e, 0xb5, 0xb0, 0xaf, 0x16, 0xd7, 0x8b, 0x1b, 0x33, 0x9b, 0xab, 0x7a,
+	0x2e, 0x74, 0x37, 0xa9, 0x85, 0x8d, 0xa5, 0x6e, 0xa8, 0xcd, 0x73, 0x46, 0x09, 0x41, 0x48, 0xc2,
+	0x3b, 0x60, 0xae, 0x4d, 0x1c, 0xd2, 0x0e, 0xda, 0xdb, 0xb4, 0xb1, 0x47, 0x1e, 0x61, 0x75, 0x7c,
+	0x5d, 0xd9, 0x98, 0xd9, 0xac, 0xe4, 0xb1, 0xcc, 0x38, 0x18, 0x3b, 0xc4, 0x67, 0xc6, 0xea, 0xd3,
+	0x50, 0x1b, 0x8b, 0x0c, 0xeb, 0x97, 0x36, 0x33, 0xe7, 0x08, 0xdf, 0x46, 0x3e, 0x3b, 0x70, 0x2d,
+	0xc4, 0xf0, 0x3e, 0x69, 0x63, 0x75, 0x82, 0xe3, 0x97, 0x75, 0x91, 0x3c, 0x3d, 0x09, 0x9c, 0xbe,
+	0x9f, 0x24, 0xcf, 0x28, 0x27, 0xd8, 0xfd, 0x92, 0x4f, 0x9e, 0x6b, 0x8a, 0x99, 0xa1, 0xc1, 0x5b,
+	0x60, 0x29, 0x70, 0x90, 0xef, 0x93, 0x96, 0x83, 0xad, 0xbb, 0xf7, 0x69, 0xe3, 0xae, 0x17, 0x38,
+	0xbe, 0x3a, 0xbd, 0x5e, 0xdc, 0x98, 0x36, 0xb4, 0x6e, 0xa8, 0x9d, 0x4d, 0xaf, 0xb7, 0x69, 0xc3,
+	0x0c, 0x1c, 0x39, 0x08, 0x8b, 0xb9, 0xcb, 0xea, 0xf3, 0x69, 0x30, 0x1e, 0x45, 0x2d, 0x4a, 0x53,
+	0xbd, 0x2f, 0x4d, 0xf5, 0xbe, 0x34, 0xd5, 0x2d, 0xb8, 0x03, 0x4a, 0x3b, 0xc8, 0x67, 0x7b, 0x18,
+	0x3b, 0x3c, 0x55, 0x27, 0x7b, 0xb5, 0x1c, 0x7b, 0xd5, 0x93, 0xe1, 0xfe, 0xf4, 0x4e, 0xf0, 0x26,
+	0x28, 0x45, 0x29, 0xd9, 0xef, 0xb8, 0x58, 0x2d, 0xc6, 0x68, 0x03, 0xf3, 0x19, 0x71, 0x18, 0xab,
+	0xdd, 0x50, 0x83, 0x09, 0xbf, 0x64, 0x57, 0x0f, 0x03, 0x5e, 0x01, 0x20, 0xe1, 0xae, 0x5b, 0x3c,
+	0xab, 0xd3, 0x86, 0xda, 0x0d, 0xb5, 0xe5, 0x94, 0x2a, 0xc9, 0x49, 0xbc, 0x70, 0x0b, 0x4c, 0x32,
+	0x44, 0x1c, 0xe6, 0xab, 0x13, 0xbc, 0xae, 0xd6, 0x74, 0xf1, 0x46, 0x74, 0xe4, 0x12, 0x3d, 0x7a,
+	0x47, 0xfa, 0xf1, 0x25, 0x7d, 0x3f, 0xe2, 0x30, 0xe6, 0x62, 0xa7, 0x62, 0x01, 0x33, 0xfe, 0x0b,
+	0x77, 0xc1, 0xa4, 0x8d, 0x1a, 0xd8, 0xf6, 0xd5, 0x49, 0x0e, 0x51, 0x1d, 0xec, 0x8a, 0xbe, 0xc3,
+	0x99, 0x6e, 0x38, 0xcc, 0xeb, 0x18, 0xcb, 0xdd, 0x50, 0x5b, 0x10, 0x52, 0x92, 0x61, 0x31, 0x4e,
+	0x54, 0x48, 0xfb, 0x94, 0x21, 0x3b, 0xa9, 0x42, 0x5f, 0x9d, 0x7a, 0xb5, 0x42, 0xed, 0x97, 0x36,
+	0x33, 0x67, 0xf8, 0x83, 0x02, 0xd6, 0xb7, 0x6c, 0x9b, 0x36, 0x11, 0x43, 0x0d, 0x1b, 0x1b, 0x9d,
+	0x5d, 0x8f, 0x50, 0x8f, 0xb0, 0xce, 0x96, 0x63, 0x25, 0x5c, 0x6a, 0x89, 0x3b, 0xf3, 0xee, 0x10,
+	0x67, 0x46, 0x89, 0x0b, 0x37, 0x37, 0x62, 0x83, 0x46, 0x6a, 0x33, 0x47, 0x72, 0xc0, 0xb7, 0x40,
+	0x29, 0x53, 0xf2, 0x2b, 0xdd, 0x50, 0x5b, 0xbc, 0x9f, 0x2b, 0xf4, 0xa9, 0x98, 0x04, 0x3d, 0xb0,
+	0x10, 0xa3, 0x62, 0xcb, 0xe8, 0x6c, 0xd3, 0x46, 0xdd, 0x52, 0x01, 0xf7, 0xea, 0xc2, 0xc9, 0x5e,
+	0xf5, 0xd8, 0x85, 0x17, 0x6a, 0xec, 0x45, 0x0e, 0xcd, 0xcc, 0x51, 0xca, 0x08, 0xcc, 0x48, 0x79,
+	0x86, 0xff, 0x05, 0xc5, 0x23, 0xdc, 0x89, 0x5f, 0xd6, 0x62, 0x37, 0xd4, 0xce, 0x1c, 0xe1, 0x8e,
+	0x64, 0x6b, 0x74, 0x0b, 0xdf, 0x00, 0x13, 0xc7, 0xc8, 0x0e, 0x70, 0xdc, 0x03, 0x79, 0x0b, 0xe3,
+	0x04, 0xb9, 0x85, 0x71, 0xc2, 0xd5, 0xc2, 0x15, 0xa5, 0xfc, 0x8d, 0x02, 0xfe, 0x7f, 0xaa, 0xf0,
+	0xcb, 0xda, 0x27, 0x86, 0x6a, 0xaf, 0xcb, 0xda, 0x47, 0xd7, 0xd8, 0x28, 0xeb, 0x3e, 0x57, 0xc0,
+	0xca, 0xc0, 0x30, 0x9e, 0x2e, 0x16, 0x7f, 0xad, 0x35, 0xd5, 0x6f, 0xc7, 0x41, 0x29, 0x79, 0xed,
+	0xa7, 0xe8, 0x72, 0x69, 0x37, 0x28, 0xfc, 0xd1, 0x6e, 0xb0, 0xdf, 0xeb, 0x06, 0x62, 0x50, 0x9d,
+	0x1f, 0xde, 0xd8, 0x5e, 0xa1, 0x23, 0x7c, 0xaa, 0x00, 0x18, 0x38, 0x3e, 0x66, 0x75, 0xc7, 0xc2,
+	0x0f, 0xb1, 0x25, 0x24, 0xd5, 0x71, 0xae, 0x62, 0xf3, 0x04, 0x15, 0x07, 0x39, 0x21, 0xa1, 0x6e,
+	0xbd, 0x1b, 0x6a, 0xe7, 0xf2, 0x88, 0x92, 0xea, 0x01, 0xfa, 0xfe, 0x8e, 0xea, 0x6e, 0x83, 0x7f,
+	0x0d, 0xb1, 0xf9, 0x75, 0xa8, 0xab, 0xfe, 0x5a, 0x04, 0x6b, 0xb7, 0x03, 0x1c, 0xe0, 0x6b, 0x76,
+	0xe0, 0x33, 0xec, 0x25, 0xd5, 0x75, 0xe0, 0xa3, 0x16, 0x86, 0x75, 0x30, 0xd5, 0xf4, 0x70, 0x54,
+	0xc9, 0x5c, 0xeb, 0xc9, 0x43, 0x6f, 0x29, 0xae, 0x88, 0x44, 0x84, 0xcf, 0xbc, 0xe4, 0x10, 0xd9,
+	0xf5, 0x20, 0xd2, 0x23, 0xdb, 0xc5, 0x09, 0xb2, 0x5d, 0x9c, 0x10, 0x4d, 0x33, 0x1c, 0x2f, 0x50,
+	0x75, 0x8b, 0xcf, 0xc7, 0x78, 0x9a, 0xa5, 0x54, 0x79, 0x9a, 0xa5, 0x54, 0xf8, 0xa5, 0x02, 0x96,
+	0x92, 0x3d, 0xce, 0x4f, 0x1b, 0x43, 0x5c, 0x27, 0xd7, 0xf3, 0x75, 0x32, 0xd4, 0xf5, 0xde, 0x33,
+	0x93, 0x60, 0x44, 0xe5, 0x9c, 0x8d, 0xdd, 0x1c, 0xa4, 0xc8, 0x1c, 0x44, 0x2c, 0x3f, 0x56, 0x80,
+	0x3a, 0x0c, 0xee, 0x9f, 0xe8, 0x51, 0xd5, 0x1f, 0x8b, 0xa0, 0x3c, 0xc8, 0x69, 0x13, 0xbb, 0xd4,
+	0x63, 0xbd, 0x95, 0x54, 0x19, 0xb1, 0x92, 0x4a, 0xd5, 0x51, 0xf8, 0x93, 0xd5, 0xf1, 0x58, 0x01,
+	0x0b, 0x52, 0xe8, 0x78, 0x5a, 0xe2, 0x06, 0x62, 0xe4, 0x9d, 0x1d, 0x6e, 0xbb, 0x9c, 0x36, 0x0e,
+	0x22, 0x72, 0x56, 0xe9, 0x86, 0x5a, 0x39, 0x8b, 0x2f, 0xf9, 0x93, 0xd3, 0x5d, 0xfe, 0x5a, 0x01,
+	0x2b, 0x03, 0xb1, 0x4e, 0xf7, 0x0a, 0x3f, 0xec, 0x4f, 0xd8, 0x9b, 0xaf, 0x50, 0x79, 0x23, 0xb3,
+	0xf7, 0x59, 0x01, 0xcc, 0xca, 0xe9, 0x86, 0x77, 0xc0, 0x74, 0xcf, 0x7e, 0x55, 0xe1, 0x41, 0xbb,
+	0x78, 0x72, 0x85, 0xa4, 0x61, 0x12, 0xf1, 0x59, 0x8c, 0x93, 0x93, 0xe2, 0x98, 0xe9, 0xbf, 0xe5,
+	0xaf, 0x14, 0x30, 0xd7, 0x2f, 0x70, 0xba, 0x20, 0x7c, 0xd4, 0x1f, 0x04, 0x5d, 0x1a, 0x26, 0xbd,
+	0xcf, 0x2f, 0xdd, 0x3d, 0x6a, 0xf1, 0xe9, 0x92, 0xa8, 0xd3, 0x6f, 0x07, 0xc8, 0x61, 0x84, 0x75,
+	0x46, 0xc6, 0xe1, 0xb7, 0x02, 0x58, 0x8c, 0x3e, 0x3d, 0x84, 0xa3, 0xc4, 0x69, 0xd5, 0x9d, 0x43,
+	0x0a, 0x37, 0x41, 0xc9, 0x26, 0x87, 0x38, 0xfa, 0x3c, 0xe4, 0xe6, 0x9d, 0x11, 0xeb, 0x73, 0x42,
+	0x93, 0xd7, 0xe7, 0x84, 0x16, 0x35, 0x1c, 0xc4, 0x3e, 0xa0, 0x3e, 0xbb, 0xe5, 0x34, 0x85, 0xb5,
+	0x25, 0xd1, 0x70, 0x52, 0xaa, 0xdc, 0x70, 0x52, 0x2a, 0x7c, 0x07, 0xcc, 0xb8, 0x1e, 0x8e, 0xe8,
+	0xa4, 0x61, 0x8b, 0x5d, 0xbe, 0x64, 0xac, 0x75, 0x43, 0x6d, 0x45, 0x22, 0x4b, 0xb2, 0x32, 0x37,
+	0x7c, 0x0f, 0xcc, 0x37, 0xa9, 0xd3, 0x0c, 0x3c, 0x0f, 0x3b, 0xcd, 0xce, 0x1e, 0x3a, 0x14, 0x1f,
+	0x64, 0x25, 0xe3, 0xdf, 0xdd, 0x50, 0x5b, 0xcb, 0x5c, 0x49, 0x20, 0x59, 0x29, 0x78, 0x0c, 0xa0,
+	0xc8, 0xb2, 0x89, 0x1f, 0x04, 0xc4, 0xc3, 0x6d, 0x9c, 0x2e, 0xf4, 0xff, 0xcb, 0x57, 0xc2, 0xad,
+	0x1c, 0xaf, 0x18, 0x87, 0x79, 0x0c, 0x79, 0x1c, 0xe6, 0x6f, 0xab, 0x5f, 0x28, 0x00, 0xe6, 0xc1,
+	0xa0, 0x0d, 0xe6, 0x5d, 0x6a, 0xf5, 0xd9, 0x22, 0xa6, 0xc7, 0x7f, 0xf2, 0xb6, 0xec, 0xf6, 0x33,
+	0x0a, 0xd7, 0x33, 0xd2, 0xa9, 0x15, 0xef, 0x8f, 0x99, 0x59, 0x68, 0x63, 0x0e, 0xcc, 0x7a, 0xb2,
+	0x51, 0xbf, 0x4c, 0x82, 0xf9, 0x0c, 0x2a, 0xf4, 0xc1, 0x6c, 0xf4, 0xad, 0xb4, 0x87, 0x6d, 0xdc,
+	0x64, 0xd4, 0x8b, 0x1f, 0xc9, 0xe5, 0x91, 0xe6, 0xf0, 0x3d, 0x22, 0x91, 0x12, 0x4f, 0xa5, 0xdc,
+	0x0d, 0xb5, 0x55, 0x19, 0x4c, 0x8a, 0x51, 0x9f, 0x12, 0xb8, 0x0b, 0x4a, 0xe8, 0xf0, 0x90, 0x38,
+	0xd1, 0x00, 0x12, 0x2f, 0xe0, 0xdc, 0xa0, 0x75, 0x6a, 0x2b, 0xe6, 0x11, 0x75, 0x9a, 0x48, 0xc8,
+	0x75, 0x9a, 0xd0, 0xe0, 0x01, 0x98, 0x61, 0xd4, 0xc6, 0x1e, 0x62, 0x84, 0x3a, 0xc9, 0x82, 0x55,
+	0x19, 0xb8, 0xa3, 0xf5, 0xd8, 0x7a, 0x8d, 0x57, 0x16, 0x35, 0xe5, 0x03, 0xa4, 0x60, 0x06, 0x39,
+	0x0e, 0x65, 0x31, 0xec, 0xd4, 0xb0, 0xa5, 0x2a, 0x1b, 0x9c, 0xad, 0x54, 0x48, 0xc4, 0x86, 0x17,
+	0xbe, 0x04, 0x25, 0x17, 0xbe, 0x44, 0x8e, 0xde, 0xa8, 0x9b, 0x8e, 0xe6, 0x68, 0xf0, 0x71, 0xdf,
+	0x13, 0x9a, 0xec, 0x7b, 0x42, 0x83, 0xdb, 0x60, 0x21, 0x79, 0x3b, 0xd4, 0xd9, 0xa5, 0x36, 0x69,
+	0x76, 0xf8, 0xcf, 0x0b, 0xd3, 0xa2, 0xb9, 0x67, 0xef, 0xe4, 0xe6, 0x9e, 0xbd, 0x83, 0x8f, 0xc0,
+	0x72, 0xd2, 0x6e, 0xfa, 0xaa, 0x74, 0x92, 0x67, 0x69, 0x63, 0x50, 0x40, 0xcd, 0x01, 0xfc, 0xc6,
+	0xb9, 0x38, 0xb4, 0x03, 0xd1, 0xcc, 0x81, 0xd4, 0x72, 0x0b, 0x2c, 0xe6, 0x8a, 0xea, 0xb5, 0x2c,
+	0x92, 0x87, 0x60, 0x21, 0x9b, 0xa0, 0xd7, 0xb2, 0x41, 0x7e, 0xa7, 0x80, 0xb5, 0xdd, 0xc0, 0xf6,
+	0x91, 0xb7, 0x97, 0x14, 0xcc, 0x36, 0x6d, 0x5c, 0xc7, 0x0c, 0x11, 0xdb, 0x8f, 0xc0, 0xc4, 0x87,
+	0xa7, 0x92, 0x82, 0x71, 0x82, 0x0c, 0xc6, 0x09, 0x11, 0xeb, 0xed, 0xec, 0x86, 0x98, 0x1d, 0xd4,
+	0x82, 0x03, 0x5e, 0x00, 0x93, 0x51, 0xe7, 0xc7, 0x2c, 0xde, 0x0e, 0xf9, 0xc7, 0x83, 0xa0, 0xc8,
+	0x1f, 0x0f, 0x82, 0x62, 0x7c, 0xf2, 0xf4, 0x45, 0x45, 0x79, 0xf6, 0xa2, 0xa2, 0xfc, 0xfc, 0xa2,
+	0xa2, 0x3c, 0x79, 0x59, 0x19, 0x7b, 0xf6, 0xb2, 0x32, 0xf6, 0xd3, 0xcb, 0xca, 0xd8, 0xc7, 0xd7,
+	0xa4, 0x9f, 0xf6, 0x90, 0xd7, 0x46, 0x16, 0x72, 0x3d, 0x1a, 0x15, 0x7b, 0x7c, 0xaa, 0x9d, 0xe2,
+	0x37, 0xcc, 0xc6, 0x24, 0xdf, 0x76, 0x2e, 0xff, 0x1e, 0x00, 0x00, 0xff, 0xff, 0x3d, 0x8a, 0x48,
+	0x6d, 0xf1, 0x14, 0x00, 0x00,
 }
 
 func (m *Executor) Marshal() (dAtA []byte, err error) {
@@ -1618,6 +1685,50 @@ func (m *PodRequirements) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	return len(dAtA) - i, nil
 }
 
+func (m *PulsarSchedulerJobDetails) Marshal() (dAtA []byte, err error) {
+	size := m.Size()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalToSizedBuffer(dAtA[:size])
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *PulsarSchedulerJobDetails) MarshalTo(dAtA []byte) (int, error) {
+	size := m.Size()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *PulsarSchedulerJobDetails) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	if len(m.JobSet) > 0 {
+		i -= len(m.JobSet)
+		copy(dAtA[i:], m.JobSet)
+		i = encodeVarintSchedulerobjects(dAtA, i, uint64(len(m.JobSet)))
+		i--
+		dAtA[i] = 0x1a
+	}
+	if len(m.Queue) > 0 {
+		i -= len(m.Queue)
+		copy(dAtA[i:], m.Queue)
+		i = encodeVarintSchedulerobjects(dAtA, i, uint64(len(m.Queue)))
+		i--
+		dAtA[i] = 0x12
+	}
+	if len(m.JobId) > 0 {
+		i -= len(m.JobId)
+		copy(dAtA[i:], m.JobId)
+		i = encodeVarintSchedulerobjects(dAtA, i, uint64(len(m.JobId)))
+		i--
+		dAtA[i] = 0xa
+	}
+	return len(dAtA) - i, nil
+}
+
 func encodeVarintSchedulerobjects(dAtA []byte, offset int, v uint64) int {
 	offset -= sovSchedulerobjects(v)
 	base := offset
@@ -1929,6 +2040,27 @@ func (m *PodRequirements) Size() (n int) {
 	return n
 }
 
+func (m *PulsarSchedulerJobDetails) Size() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	l = len(m.JobId)
+	if l > 0 {
+		n += 1 + l + sovSchedulerobjects(uint64(l))
+	}
+	l = len(m.Queue)
+	if l > 0 {
+		n += 1 + l + sovSchedulerobjects(uint64(l))
+	}
+	l = len(m.JobSet)
+	if l > 0 {
+		n += 1 + l + sovSchedulerobjects(uint64(l))
+	}
+	return n
+}
+
 func sovSchedulerobjects(x uint64) (n int) {
 	return (math_bits.Len64(x|1) + 6) / 7
 }
@@ -4595,6 +4727,152 @@ func (m *PodRequirements) Unmarshal(dAtA []byte) error {
 	}
 	return nil
 }
+func (m *PulsarSchedulerJobDetails) Unmarshal(dAtA []byte) error {
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return ErrIntOverflowSchedulerobjects
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= uint64(b&0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: PulsarSchedulerJobDetails: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: PulsarSchedulerJobDetails: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field JobId", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowSchedulerobjects
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				stringLen |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return ErrInvalidLengthSchedulerobjects
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex < 0 {
+				return ErrInvalidLengthSchedulerobjects
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.JobId = string(dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
+		case 2:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Queue", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowSchedulerobjects
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				stringLen |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return ErrInvalidLengthSchedulerobjects
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex < 0 {
+				return ErrInvalidLengthSchedulerobjects
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Queue = string(dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
+		case 3:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field JobSet", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowSchedulerobjects
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				stringLen |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return ErrInvalidLengthSchedulerobjects
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex < 0 {
+				return ErrInvalidLengthSchedulerobjects
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.JobSet = string(dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
+		default:
+			iNdEx = preIndex
+			skippy, err := skipSchedulerobjects(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if (skippy < 0) || (iNdEx+skippy) < 0 {
+				return ErrInvalidLengthSchedulerobjects
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			iNdEx += skippy
+		}
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
 func skipSchedulerobjects(dAtA []byte) (n int, err error) {
 	l := len(dAtA)
 	iNdEx := 0
diff --git a/internal/scheduler/schedulerobjects/schedulerobjects.proto b/internal/scheduler/schedulerobjects/schedulerobjects.proto
index 796bcba0..8bf89bd0 100644
--- a/internal/scheduler/schedulerobjects/schedulerobjects.proto
+++ b/internal/scheduler/schedulerobjects/schedulerobjects.proto
@@ -129,4 +129,12 @@ message PodRequirements {
     string preemptionPolicy = 5;
     // Sum of the resource requirements for all containers that make up this pod.
     k8s.io.api.core.v1.ResourceRequirements resourceRequirements = 6 [(gogoproto.nullable) = false];
+}
+
+// Used to store details about pulsar scheduler jobs in Redis
+// Can be removed once we deprecate the legacy scheduler
+message PulsarSchedulerJobDetails  {
+    string JobId = 1;
+    string Queue = 2;
+    string JobSet = 3;
 }
\ No newline at end of file
diff --git a/internal/scheduler/submitcheck.go b/internal/scheduler/submitcheck.go
index 804caf47..1fe6fd43 100644
--- a/internal/scheduler/submitcheck.go
+++ b/internal/scheduler/submitcheck.go
@@ -1,63 +1,114 @@
 package scheduler
 
 import (
+	"context"
 	"fmt"
 	"strings"
 	"sync"
 	"time"
 
+	log "github.com/sirupsen/logrus"
 	"golang.org/x/exp/maps"
+	"k8s.io/apimachinery/pkg/util/clock"
 
 	"github.com/armadaproject/armada/internal/armada/configuration"
+	"github.com/armadaproject/armada/internal/scheduler/database"
 	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
 	"github.com/armadaproject/armada/pkg/api"
 )
 
+type minimalExecutor struct {
+	nodeDb     *NodeDb
+	updateTime time.Time
+}
+
 type SubmitChecker struct {
-	executorTimeout  time.Duration
-	priorityClasses  map[string]configuration.PriorityClass
-	gangIdAnnotation string
-	nodeDbByExecutor map[string]*NodeDb
-	mu               sync.Mutex
+	executorTimeout    time.Duration
+	priorityClasses    map[string]configuration.PriorityClass
+	gangIdAnnotation   string
+	executorById       map[string]minimalExecutor
+	priorities         []int32
+	indexedResources   []string
+	indexedTaints      []string
+	indexedNodeLabels  []string
+	executorRepository database.ExecutorRepository
+	clock              clock.Clock
+	mu                 sync.Mutex
 }
 
-func NewSubmitChecker(executorTimeout time.Duration, priorityClasses map[string]configuration.PriorityClass, gangIdAnnotation string) *SubmitChecker {
+func NewSubmitChecker(
+	executorTimeout time.Duration,
+	schedulingConfig configuration.SchedulingConfig,
+	executorRepository database.ExecutorRepository,
+) *SubmitChecker {
 	return &SubmitChecker{
-		executorTimeout:  executorTimeout,
-		priorityClasses:  priorityClasses,
-		gangIdAnnotation: gangIdAnnotation,
+		executorTimeout:    executorTimeout,
+		priorityClasses:    schedulingConfig.Preemption.PriorityClasses,
+		gangIdAnnotation:   schedulingConfig.GangIdAnnotation,
+		executorById:       map[string]minimalExecutor{},
+		priorities:         schedulingConfig.Preemption.AllowedPriorities(),
+		indexedResources:   schedulingConfig.IndexedResources,
+		indexedTaints:      schedulingConfig.IndexedTaints,
+		indexedNodeLabels:  schedulingConfig.IndexedNodeLabels,
+		executorRepository: executorRepository,
+		clock:              clock.RealClock{},
 	}
 }
 
-// RegisterNodeDb adds a NodeDb to use when checking if a pod can be scheduled.
-// To only check static scheduling requirements, set NodeDb.CheckOnlyStaticRequirements = true
-// before registering it.
-func (srv *SubmitChecker) RegisterNodeDb(executor string, nodeDb *NodeDb) {
-	srv.mu.Lock()
-	defer srv.mu.Unlock()
-	if srv.nodeDbByExecutor == nil {
-		srv.nodeDbByExecutor = make(map[string]*NodeDb)
+func (srv *SubmitChecker) Run(ctx context.Context) error {
+	srv.updateExecutors(ctx)
+	ticker := time.NewTicker(1 * time.Minute)
+	for {
+		select {
+		case <-ctx.Done():
+			return nil
+		case <-ticker.C:
+			srv.updateExecutors(ctx)
+		}
+	}
+}
+
+func (srv *SubmitChecker) updateExecutors(ctx context.Context) {
+	executors, err := srv.executorRepository.GetExecutors(ctx)
+	if err != nil {
+		log.WithError(err).Error("Error fetching executors")
+		return
+	}
+	for _, executor := range executors {
+		nodeDb, err := srv.constructNodeDb(executor.Nodes)
+		if err == nil {
+			srv.mu.Lock()
+			srv.executorById[executor.Id] = minimalExecutor{
+				nodeDb:     nodeDb,
+				updateTime: executor.LastUpdateTime,
+			}
+			srv.mu.Unlock()
+			if err != nil {
+				log.WithError(err).Errorf("Error constructing node db for executor %s", executor.Id)
+			}
+		} else {
+			log.WithError(err).Warnf("Error clearing nodedb for executor %s", executor.Id)
+		}
+
 	}
-	srv.nodeDbByExecutor[executor] = nodeDb
 }
 
 func (srv *SubmitChecker) CheckApiJobs(jobs []*api.Job) (bool, string) {
-	// // First, check if all jobs can be scheduled individually.
-	// // TODO: Disabled for now; the SubmitChecker would reject all jobs until populated.
-	// for i, job := range jobs {
-	// 	reqs := PodRequirementsFromJob(srv.priorityClasses, job)
-	// 	canSchedule, reason := srv.Check(reqs)
-	// 	if !canSchedule {
-	// 		return canSchedule, fmt.Sprintf("%d-th job unschedulable:\n%s", i, reason)
-	// 	}
-	// }
+	// First, check if all jobs can be scheduled individually.
+	for i, job := range jobs {
+		reqs := PodRequirementsFromLegacySchedulerJob(job, srv.priorityClasses)
+		canSchedule, reason := srv.check([]*schedulerobjects.PodRequirements{reqs})
+		if !canSchedule {
+			return canSchedule, fmt.Sprintf("%d-th job unschedulable:\n%s", i, reason)
+		}
+	}
 	// Then, check if all gangs can be scheduled.
-	for gangId, jobs := range groupJobsByAnnotation(srv.gangIdAnnotation, jobs) {
+	for gangId, jobs := range GroupJobsByAnnotation(srv.gangIdAnnotation, jobs) {
 		if gangId == "" {
 			continue
 		}
 		reqs := PodRequirementsFromLegacySchedulerJobs(jobs, srv.priorityClasses)
-		canSchedule, reason := srv.Check(reqs)
+		canSchedule, reason := srv.check(reqs)
 		if !canSchedule {
 			return canSchedule, fmt.Sprintf("gang %s is unschedulable:\n%s", gangId, reason)
 		}
@@ -65,7 +116,7 @@ func (srv *SubmitChecker) CheckApiJobs(jobs []*api.Job) (bool, string) {
 	return true, ""
 }
 
-func groupJobsByAnnotation(annotation string, jobs []*api.Job) map[string][]*api.Job {
+func GroupJobsByAnnotation(annotation string, jobs []*api.Job) map[string][]*api.Job {
 	rv := make(map[string][]*api.Job)
 	for _, job := range jobs {
 		if len(job.Annotations) == 0 {
@@ -79,29 +130,30 @@ func groupJobsByAnnotation(annotation string, jobs []*api.Job) map[string][]*api
 }
 
 // Check if a set of pods can be scheduled onto some cluster.
-func (srv *SubmitChecker) Check(reqs []*schedulerobjects.PodRequirements) (bool, string) {
+func (srv *SubmitChecker) check(reqs []*schedulerobjects.PodRequirements) (bool, string) {
 	if len(reqs) == 0 {
 		return true, ""
 	}
 
 	// Make a shallow copy to avoid holding the lock and
-	// preventing registering new NodeDbs while checking if jobs can be scheduled.
+	// preventing updating NodeDbs while checking if jobs can be scheduled.
 	srv.mu.Lock()
-	nodeDbByExecutor := maps.Clone(srv.nodeDbByExecutor)
+	executorById := maps.Clone(srv.executorById)
 	srv.mu.Unlock()
-	nodeDbByExecutor = srv.filterStaleNodeDbs(nodeDbByExecutor)
-	if len(nodeDbByExecutor) == 0 {
+	executorById = srv.filterStaleNodeDbs(executorById)
+	if len(executorById) == 0 {
 		return false, "no executor clusters available"
 	}
 
 	canSchedule := false
 	var sb strings.Builder
-	for executor, nodeDb := range nodeDbByExecutor {
+	for id, executor := range executorById {
+		nodeDb := executor.nodeDb
 		txn := nodeDb.db.Txn(true)
 		reports, ok, err := nodeDb.ScheduleManyWithTxn(txn, reqs)
 		txn.Abort()
 
-		sb.WriteString(executor)
+		sb.WriteString(id)
 		if err != nil {
 			sb.WriteString(err.Error())
 			sb.WriteString("\n")
@@ -132,12 +184,34 @@ func (srv *SubmitChecker) Check(reqs []*schedulerobjects.PodRequirements) (bool,
 	return canSchedule, sb.String()
 }
 
-func (srv *SubmitChecker) filterStaleNodeDbs(nodeDbByExecutor map[string]*NodeDb) map[string]*NodeDb {
-	rv := make(map[string]*NodeDb)
-	for executor, nodeDb := range nodeDbByExecutor {
-		if time.Since(nodeDb.TimeOfMostRecentUpsert()) < srv.executorTimeout {
-			rv[executor] = nodeDb
+func (srv *SubmitChecker) filterStaleNodeDbs(executorsById map[string]minimalExecutor) map[string]minimalExecutor {
+	rv := make(map[string]minimalExecutor)
+	for id, executor := range executorsById {
+		if srv.clock.Since(executor.updateTime) < srv.executorTimeout {
+			rv[id] = executor
 		}
 	}
 	return rv
 }
+
+func (srv *SubmitChecker) constructNodeDb(nodes []*schedulerobjects.Node) (*NodeDb, error) {
+	// Nodes to be considered by the scheduler.
+	nodeDb, err := NewNodeDb(
+		srv.priorities,
+		srv.indexedResources,
+		srv.indexedTaints,
+		srv.indexedNodeLabels,
+	)
+	if err != nil {
+		return nil, err
+	}
+	err = nodeDb.Upsert(nodes)
+	if err != nil {
+		return nil, err
+	}
+	err = nodeDb.ClearAllocated()
+	if err != nil {
+		return nil, err
+	}
+	return nodeDb, nil
+}
diff --git a/internal/scheduleringester/ingester.go b/internal/scheduleringester/ingester.go
index d7b1bc40..a43fef6b 100644
--- a/internal/scheduleringester/ingester.go
+++ b/internal/scheduleringester/ingester.go
@@ -3,6 +3,8 @@ package scheduleringester
 import (
 	"time"
 
+	"github.com/armadaproject/armada/internal/common/schedulers"
+
 	"github.com/pkg/errors"
 	log "github.com/sirupsen/logrus"
 
@@ -11,7 +13,6 @@ import (
 	"github.com/armadaproject/armada/internal/common/database"
 	"github.com/armadaproject/armada/internal/common/ingest"
 	"github.com/armadaproject/armada/internal/common/ingest/metrics"
-	"github.com/armadaproject/armada/pkg/armadaevents"
 )
 
 // Run will create a pipeline that will take Armada event messages from Pulsar and update the
@@ -26,26 +27,18 @@ func Run(config Configuration) {
 	}
 	schedulerDb := NewSchedulerDb(db, svcMetrics, 100*time.Millisecond, 60*time.Second)
 
-	// Discard submit job messages not intended for this scheduler.
-	submitJobFilter := func(event *armadaevents.EventSequence_Event) bool {
-		// Discard if a SubmitJob event that doesn't target this scheduler.
-		if e := event.GetSubmitJob(); e != nil && e.Scheduler != "pulsar" {
-			return false
-		}
-		return true
-	}
-
 	compressor, err := compress.NewZlibCompressor(1024)
 	if err != nil {
 		panic(errors.WithMessage(err, "Error creating  compressor"))
 	}
-	converter := NewInstructionConverter(svcMetrics, submitJobFilter, config.PriorityClasses, compressor)
+	converter := NewInstructionConverter(svcMetrics, config.PriorityClasses, compressor)
 
-	ingester := ingest.NewIngestionPipeline(
+	ingester := ingest.NewFilteredMsgIngestionPipeline(
 		config.Pulsar,
 		config.SubscriptionName,
 		config.BatchSize,
 		config.BatchDuration,
+		schedulers.ForPulsarScheduler,
 		converter,
 		schedulerDb,
 		config.Metrics,
diff --git a/internal/scheduleringester/instructions.go b/internal/scheduleringester/instructions.go
index 0f72178c..343963d0 100644
--- a/internal/scheduleringester/instructions.go
+++ b/internal/scheduleringester/instructions.go
@@ -28,20 +28,17 @@ type eventSequenceCommon struct {
 
 type InstructionConverter struct {
 	metrics         *metrics.Metrics
-	eventFilter     func(event *armadaevents.EventSequence_Event) bool
 	priorityClasses map[string]configuration.PriorityClass
 	compressor      compress.Compressor
 }
 
 func NewInstructionConverter(
 	metrics *metrics.Metrics,
-	filter func(event *armadaevents.EventSequence_Event) bool,
 	priorityClasses map[string]configuration.PriorityClass,
 	compressor compress.Compressor,
 ) ingest.InstructionConverter[*DbOperationsWithMessageIds] {
 	return &InstructionConverter{
 		metrics:         metrics,
-		eventFilter:     filter,
 		priorityClasses: priorityClasses,
 		compressor:      compressor,
 	}
@@ -70,53 +67,51 @@ func (c *InstructionConverter) convertSequence(es *armadaevents.EventSequence) [
 
 	operations := make([]DbOperation, 0, len(es.Events))
 	for idx, event := range es.Events {
-		if c.eventFilter(event) {
-			var err error = nil
-			var operationsFromEvent []DbOperation
-			switch event.GetEvent().(type) {
-			case *armadaevents.EventSequence_Event_SubmitJob:
-				operationsFromEvent, err = c.handleSubmitJob(event.GetSubmitJob(), meta)
-			case *armadaevents.EventSequence_Event_JobRunLeased:
-				operationsFromEvent, err = c.handleJobRunLeased(event.GetJobRunLeased(), meta)
-			case *armadaevents.EventSequence_Event_JobRunRunning:
-				operationsFromEvent, err = c.handleJobRunRunning(event.GetJobRunRunning())
-			case *armadaevents.EventSequence_Event_JobRunSucceeded:
-				operationsFromEvent, err = c.handleJobRunSucceeded(event.GetJobRunSucceeded())
-			case *armadaevents.EventSequence_Event_JobRunErrors:
-				operationsFromEvent, err = c.handleJobRunErrors(event.GetJobRunErrors())
-			case *armadaevents.EventSequence_Event_JobSucceeded:
-				operationsFromEvent, err = c.handleJobSucceeded(event.GetJobSucceeded())
-			case *armadaevents.EventSequence_Event_JobErrors:
-				operationsFromEvent, err = c.handleJobErrors(event.GetJobErrors())
-			case *armadaevents.EventSequence_Event_ReprioritiseJob:
-				operationsFromEvent, err = c.handleReprioritiseJob(event.GetReprioritiseJob())
-			case *armadaevents.EventSequence_Event_ReprioritiseJobSet:
-				operationsFromEvent, err = c.handleReprioritiseJobSet(event.GetReprioritiseJobSet(), meta)
-			case *armadaevents.EventSequence_Event_CancelJob:
-				operationsFromEvent, err = c.handleCancelJob(event.GetCancelJob())
-			case *armadaevents.EventSequence_Event_CancelJobSet:
-				operationsFromEvent, err = c.handleCancelJobSet(meta.jobset)
-			case *armadaevents.EventSequence_Event_CancelledJob:
-				operationsFromEvent, err = c.handleCancelledJob(event.GetCancelledJob())
-			case *armadaevents.EventSequence_Event_PartitionMarker:
-				operationsFromEvent, err = c.handlePartitionMarker(event.GetPartitionMarker(), *event.Created)
-			case *armadaevents.EventSequence_Event_ReprioritisedJob,
-				*armadaevents.EventSequence_Event_JobDuplicateDetected,
-				*armadaevents.EventSequence_Event_ResourceUtilisation,
-				*armadaevents.EventSequence_Event_StandaloneIngressInfo,
-				*armadaevents.EventSequence_Event_JobRunPreempted:
-				// These events can all be safely ignored
-				log.Debugf("Ignoring event type %T", event)
-			default:
-				// This is an event type we haven't considered. Log a warning
-				log.Warnf("Ignoring unknown event type %T", event)
-			}
-			if err != nil {
-				c.metrics.RecordPulsarMessageError(metrics.PulsarMessageErrorProcessing)
-				log.WithError(err).Warnf("Could not convert event at index %d.", idx)
-			} else {
-				operations = append(operations, operationsFromEvent...)
-			}
+		var err error = nil
+		var operationsFromEvent []DbOperation
+		switch event.GetEvent().(type) {
+		case *armadaevents.EventSequence_Event_SubmitJob:
+			operationsFromEvent, err = c.handleSubmitJob(event.GetSubmitJob(), meta)
+		case *armadaevents.EventSequence_Event_JobRunLeased:
+			operationsFromEvent, err = c.handleJobRunLeased(event.GetJobRunLeased(), meta)
+		case *armadaevents.EventSequence_Event_JobRunRunning:
+			operationsFromEvent, err = c.handleJobRunRunning(event.GetJobRunRunning())
+		case *armadaevents.EventSequence_Event_JobRunSucceeded:
+			operationsFromEvent, err = c.handleJobRunSucceeded(event.GetJobRunSucceeded())
+		case *armadaevents.EventSequence_Event_JobRunErrors:
+			operationsFromEvent, err = c.handleJobRunErrors(event.GetJobRunErrors())
+		case *armadaevents.EventSequence_Event_JobSucceeded:
+			operationsFromEvent, err = c.handleJobSucceeded(event.GetJobSucceeded())
+		case *armadaevents.EventSequence_Event_JobErrors:
+			operationsFromEvent, err = c.handleJobErrors(event.GetJobErrors())
+		case *armadaevents.EventSequence_Event_ReprioritiseJob:
+			operationsFromEvent, err = c.handleReprioritiseJob(event.GetReprioritiseJob())
+		case *armadaevents.EventSequence_Event_ReprioritiseJobSet:
+			operationsFromEvent, err = c.handleReprioritiseJobSet(event.GetReprioritiseJobSet(), meta)
+		case *armadaevents.EventSequence_Event_CancelJob:
+			operationsFromEvent, err = c.handleCancelJob(event.GetCancelJob())
+		case *armadaevents.EventSequence_Event_CancelJobSet:
+			operationsFromEvent, err = c.handleCancelJobSet(meta.jobset)
+		case *armadaevents.EventSequence_Event_CancelledJob:
+			operationsFromEvent, err = c.handleCancelledJob(event.GetCancelledJob())
+		case *armadaevents.EventSequence_Event_PartitionMarker:
+			operationsFromEvent, err = c.handlePartitionMarker(event.GetPartitionMarker(), *event.Created)
+		case *armadaevents.EventSequence_Event_ReprioritisedJob,
+			*armadaevents.EventSequence_Event_JobDuplicateDetected,
+			*armadaevents.EventSequence_Event_ResourceUtilisation,
+			*armadaevents.EventSequence_Event_StandaloneIngressInfo,
+			*armadaevents.EventSequence_Event_JobRunPreempted:
+			// These events can all be safely ignored
+			log.Debugf("Ignoring event type %T", event)
+		default:
+			// This is an event type we haven't considered. Log a warning
+			log.Warnf("Ignoring unknown event type %T", event)
+		}
+		if err != nil {
+			c.metrics.RecordPulsarMessageError(metrics.PulsarMessageErrorProcessing)
+			log.WithError(err).Warnf("Could not convert event at index %d.", idx)
+		} else {
+			operations = append(operations, operationsFromEvent...)
 		}
 	}
 	return operations
diff --git a/pkg/armadaevents/events.pb.go b/pkg/armadaevents/events.pb.go
index c298f0f3..97854a1a 100644
--- a/pkg/armadaevents/events.pb.go
+++ b/pkg/armadaevents/events.pb.go
@@ -32,6 +32,34 @@ var _ = time.Kitchen
 // proto package needs to be updated.
 const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
 
+type JobState int32
+
+const (
+	JobState_QUEUED  JobState = 0
+	JobState_PENDING JobState = 1
+	JobState_RUNNING JobState = 2
+)
+
+var JobState_name = map[int32]string{
+	0: "QUEUED",
+	1: "PENDING",
+	2: "RUNNING",
+}
+
+var JobState_value = map[string]int32{
+	"QUEUED":  0,
+	"PENDING": 1,
+	"RUNNING": 2,
+}
+
+func (x JobState) String() string {
+	return proto.EnumName(JobState_name, int32(x))
+}
+
+func (JobState) EnumDescriptor() ([]byte, []int) {
+	return fileDescriptor_6aab92ca59e015f8, []int{0}
+}
+
 // Reason reported by Kubernetes.
 type KubernetesReason int32
 
@@ -61,7 +89,7 @@ func (x KubernetesReason) String() string {
 }
 
 func (KubernetesReason) EnumDescriptor() ([]byte, []int) {
-	return fileDescriptor_6aab92ca59e015f8, []int{0}
+	return fileDescriptor_6aab92ca59e015f8, []int{1}
 }
 
 // Message representing a sequence of state transitions.
@@ -1296,15 +1324,63 @@ func (m *CancelJob) GetJobId() *Uuid {
 	return nil
 }
 
+// Filter to be used when cancelling job sets
+// This allows users to cancel all jobs in a given state for a specific job set
+type JobSetFilter struct {
+	States []JobState `protobuf:"varint,1,rep,packed,name=states,proto3,enum=armadaevents.JobState" json:"states,omitempty"`
+}
+
+func (m *JobSetFilter) Reset()         { *m = JobSetFilter{} }
+func (m *JobSetFilter) String() string { return proto.CompactTextString(m) }
+func (*JobSetFilter) ProtoMessage()    {}
+func (*JobSetFilter) Descriptor() ([]byte, []int) {
+	return fileDescriptor_6aab92ca59e015f8, []int{12}
+}
+func (m *JobSetFilter) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *JobSetFilter) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	if deterministic {
+		return xxx_messageInfo_JobSetFilter.Marshal(b, m, deterministic)
+	} else {
+		b = b[:cap(b)]
+		n, err := m.MarshalToSizedBuffer(b)
+		if err != nil {
+			return nil, err
+		}
+		return b[:n], nil
+	}
+}
+func (m *JobSetFilter) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_JobSetFilter.Merge(m, src)
+}
+func (m *JobSetFilter) XXX_Size() int {
+	return m.Size()
+}
+func (m *JobSetFilter) XXX_DiscardUnknown() {
+	xxx_messageInfo_JobSetFilter.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_JobSetFilter proto.InternalMessageInfo
+
+func (m *JobSetFilter) GetStates() []JobState {
+	if m != nil {
+		return m.States
+	}
+	return nil
+}
+
 // Request to cancel all jobs in a job set.
+// Empty set of states means all states
 type CancelJobSet struct {
+	States []JobState `protobuf:"varint,1,rep,packed,name=states,proto3,enum=armadaevents.JobState" json:"states,omitempty"`
 }
 
 func (m *CancelJobSet) Reset()         { *m = CancelJobSet{} }
 func (m *CancelJobSet) String() string { return proto.CompactTextString(m) }
 func (*CancelJobSet) ProtoMessage()    {}
 func (*CancelJobSet) Descriptor() ([]byte, []int) {
-	return fileDescriptor_6aab92ca59e015f8, []int{12}
+	return fileDescriptor_6aab92ca59e015f8, []int{13}
 }
 func (m *CancelJobSet) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1333,6 +1409,13 @@ func (m *CancelJobSet) XXX_DiscardUnknown() {
 
 var xxx_messageInfo_CancelJobSet proto.InternalMessageInfo
 
+func (m *CancelJobSet) GetStates() []JobState {
+	if m != nil {
+		return m.States
+	}
+	return nil
+}
+
 // Generated by the scheduler in response to CancelJob and CancelJobSet.
 // One such message is generated per job that was cancelled.
 type CancelledJob struct {
@@ -1343,7 +1426,7 @@ func (m *CancelledJob) Reset()         { *m = CancelledJob{} }
 func (m *CancelledJob) String() string { return proto.CompactTextString(m) }
 func (*CancelledJob) ProtoMessage()    {}
 func (*CancelledJob) Descriptor() ([]byte, []int) {
-	return fileDescriptor_6aab92ca59e015f8, []int{13}
+	return fileDescriptor_6aab92ca59e015f8, []int{14}
 }
 func (m *CancelledJob) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1391,7 +1474,7 @@ func (m *JobSucceeded) Reset()         { *m = JobSucceeded{} }
 func (m *JobSucceeded) String() string { return proto.CompactTextString(m) }
 func (*JobSucceeded) ProtoMessage()    {}
 func (*JobSucceeded) Descriptor() ([]byte, []int) {
-	return fileDescriptor_6aab92ca59e015f8, []int{14}
+	return fileDescriptor_6aab92ca59e015f8, []int{15}
 }
 func (m *JobSucceeded) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1446,7 +1529,7 @@ func (m *JobRunLeased) Reset()         { *m = JobRunLeased{} }
 func (m *JobRunLeased) String() string { return proto.CompactTextString(m) }
 func (*JobRunLeased) ProtoMessage()    {}
 func (*JobRunLeased) Descriptor() ([]byte, []int) {
-	return fileDescriptor_6aab92ca59e015f8, []int{15}
+	return fileDescriptor_6aab92ca59e015f8, []int{16}
 }
 func (m *JobRunLeased) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1510,7 +1593,7 @@ func (m *JobRunAssigned) Reset()         { *m = JobRunAssigned{} }
 func (m *JobRunAssigned) String() string { return proto.CompactTextString(m) }
 func (*JobRunAssigned) ProtoMessage()    {}
 func (*JobRunAssigned) Descriptor() ([]byte, []int) {
-	return fileDescriptor_6aab92ca59e015f8, []int{16}
+	return fileDescriptor_6aab92ca59e015f8, []int{17}
 }
 func (m *JobRunAssigned) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1573,7 +1656,7 @@ func (m *JobRunRunning) Reset()         { *m = JobRunRunning{} }
 func (m *JobRunRunning) String() string { return proto.CompactTextString(m) }
 func (*JobRunRunning) ProtoMessage()    {}
 func (*JobRunRunning) Descriptor() ([]byte, []int) {
-	return fileDescriptor_6aab92ca59e015f8, []int{17}
+	return fileDescriptor_6aab92ca59e015f8, []int{18}
 }
 func (m *JobRunRunning) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1636,7 +1719,7 @@ func (m *KubernetesResourceInfo) Reset()         { *m = KubernetesResourceInfo{}
 func (m *KubernetesResourceInfo) String() string { return proto.CompactTextString(m) }
 func (*KubernetesResourceInfo) ProtoMessage()    {}
 func (*KubernetesResourceInfo) Descriptor() ([]byte, []int) {
-	return fileDescriptor_6aab92ca59e015f8, []int{18}
+	return fileDescriptor_6aab92ca59e015f8, []int{19}
 }
 func (m *KubernetesResourceInfo) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1727,7 +1810,7 @@ func (m *PodInfo) Reset()         { *m = PodInfo{} }
 func (m *PodInfo) String() string { return proto.CompactTextString(m) }
 func (*PodInfo) ProtoMessage()    {}
 func (*PodInfo) Descriptor() ([]byte, []int) {
-	return fileDescriptor_6aab92ca59e015f8, []int{19}
+	return fileDescriptor_6aab92ca59e015f8, []int{20}
 }
 func (m *PodInfo) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1782,7 +1865,7 @@ func (m *IngressInfo) Reset()         { *m = IngressInfo{} }
 func (m *IngressInfo) String() string { return proto.CompactTextString(m) }
 func (*IngressInfo) ProtoMessage()    {}
 func (*IngressInfo) Descriptor() ([]byte, []int) {
-	return fileDescriptor_6aab92ca59e015f8, []int{20}
+	return fileDescriptor_6aab92ca59e015f8, []int{21}
 }
 func (m *IngressInfo) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1845,7 +1928,7 @@ func (m *StandaloneIngressInfo) Reset()         { *m = StandaloneIngressInfo{} }
 func (m *StandaloneIngressInfo) String() string { return proto.CompactTextString(m) }
 func (*StandaloneIngressInfo) ProtoMessage()    {}
 func (*StandaloneIngressInfo) Descriptor() ([]byte, []int) {
-	return fileDescriptor_6aab92ca59e015f8, []int{21}
+	return fileDescriptor_6aab92ca59e015f8, []int{22}
 }
 func (m *StandaloneIngressInfo) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1944,7 +2027,7 @@ func (m *JobRunSucceeded) Reset()         { *m = JobRunSucceeded{} }
 func (m *JobRunSucceeded) String() string { return proto.CompactTextString(m) }
 func (*JobRunSucceeded) ProtoMessage()    {}
 func (*JobRunSucceeded) Descriptor() ([]byte, []int) {
-	return fileDescriptor_6aab92ca59e015f8, []int{22}
+	return fileDescriptor_6aab92ca59e015f8, []int{23}
 }
 func (m *JobRunSucceeded) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -2007,7 +2090,7 @@ func (m *JobErrors) Reset()         { *m = JobErrors{} }
 func (m *JobErrors) String() string { return proto.CompactTextString(m) }
 func (*JobErrors) ProtoMessage()    {}
 func (*JobErrors) Descriptor() ([]byte, []int) {
-	return fileDescriptor_6aab92ca59e015f8, []int{23}
+	return fileDescriptor_6aab92ca59e015f8, []int{24}
 }
 func (m *JobErrors) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -2068,7 +2151,7 @@ func (m *JobRunErrors) Reset()         { *m = JobRunErrors{} }
 func (m *JobRunErrors) String() string { return proto.CompactTextString(m) }
 func (*JobRunErrors) ProtoMessage()    {}
 func (*JobRunErrors) Descriptor() ([]byte, []int) {
-	return fileDescriptor_6aab92ca59e015f8, []int{24}
+	return fileDescriptor_6aab92ca59e015f8, []int{25}
 }
 func (m *JobRunErrors) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -2148,7 +2231,7 @@ func (m *Error) Reset()         { *m = Error{} }
 func (m *Error) String() string { return proto.CompactTextString(m) }
 func (*Error) ProtoMessage()    {}
 func (*Error) Descriptor() ([]byte, []int) {
-	return fileDescriptor_6aab92ca59e015f8, []int{25}
+	return fileDescriptor_6aab92ca59e015f8, []int{26}
 }
 func (m *Error) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -2325,7 +2408,7 @@ func (m *KubernetesError) Reset()         { *m = KubernetesError{} }
 func (m *KubernetesError) String() string { return proto.CompactTextString(m) }
 func (*KubernetesError) ProtoMessage()    {}
 func (*KubernetesError) Descriptor() ([]byte, []int) {
-	return fileDescriptor_6aab92ca59e015f8, []int{26}
+	return fileDescriptor_6aab92ca59e015f8, []int{27}
 }
 func (m *KubernetesError) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -2383,7 +2466,7 @@ func (m *PodError) Reset()         { *m = PodError{} }
 func (m *PodError) String() string { return proto.CompactTextString(m) }
 func (*PodError) ProtoMessage()    {}
 func (*PodError) Descriptor() ([]byte, []int) {
-	return fileDescriptor_6aab92ca59e015f8, []int{27}
+	return fileDescriptor_6aab92ca59e015f8, []int{28}
 }
 func (m *PodError) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -2471,7 +2554,7 @@ func (m *ContainerError) Reset()         { *m = ContainerError{} }
 func (m *ContainerError) String() string { return proto.CompactTextString(m) }
 func (*ContainerError) ProtoMessage()    {}
 func (*ContainerError) Descriptor() ([]byte, []int) {
-	return fileDescriptor_6aab92ca59e015f8, []int{28}
+	return fileDescriptor_6aab92ca59e015f8, []int{29}
 }
 func (m *ContainerError) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -2547,7 +2630,7 @@ func (m *PodLeaseReturned) Reset()         { *m = PodLeaseReturned{} }
 func (m *PodLeaseReturned) String() string { return proto.CompactTextString(m) }
 func (*PodLeaseReturned) ProtoMessage()    {}
 func (*PodLeaseReturned) Descriptor() ([]byte, []int) {
-	return fileDescriptor_6aab92ca59e015f8, []int{29}
+	return fileDescriptor_6aab92ca59e015f8, []int{30}
 }
 func (m *PodLeaseReturned) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -2618,7 +2701,7 @@ func (m *PodTerminated) Reset()         { *m = PodTerminated{} }
 func (m *PodTerminated) String() string { return proto.CompactTextString(m) }
 func (*PodTerminated) ProtoMessage()    {}
 func (*PodTerminated) Descriptor() ([]byte, []int) {
-	return fileDescriptor_6aab92ca59e015f8, []int{30}
+	return fileDescriptor_6aab92ca59e015f8, []int{31}
 }
 func (m *PodTerminated) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -2682,7 +2765,7 @@ func (m *ExecutorError) Reset()         { *m = ExecutorError{} }
 func (m *ExecutorError) String() string { return proto.CompactTextString(m) }
 func (*ExecutorError) ProtoMessage()    {}
 func (*ExecutorError) Descriptor() ([]byte, []int) {
-	return fileDescriptor_6aab92ca59e015f8, []int{31}
+	return fileDescriptor_6aab92ca59e015f8, []int{32}
 }
 func (m *ExecutorError) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -2723,7 +2806,7 @@ func (m *PodUnschedulable) Reset()         { *m = PodUnschedulable{} }
 func (m *PodUnschedulable) String() string { return proto.CompactTextString(m) }
 func (*PodUnschedulable) ProtoMessage()    {}
 func (*PodUnschedulable) Descriptor() ([]byte, []int) {
-	return fileDescriptor_6aab92ca59e015f8, []int{32}
+	return fileDescriptor_6aab92ca59e015f8, []int{33}
 }
 func (m *PodUnschedulable) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -2787,7 +2870,7 @@ func (m *LeaseExpired) Reset()         { *m = LeaseExpired{} }
 func (m *LeaseExpired) String() string { return proto.CompactTextString(m) }
 func (*LeaseExpired) ProtoMessage()    {}
 func (*LeaseExpired) Descriptor() ([]byte, []int) {
-	return fileDescriptor_6aab92ca59e015f8, []int{33}
+	return fileDescriptor_6aab92ca59e015f8, []int{34}
 }
 func (m *LeaseExpired) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -2823,7 +2906,7 @@ func (m *MaxRunsExceeded) Reset()         { *m = MaxRunsExceeded{} }
 func (m *MaxRunsExceeded) String() string { return proto.CompactTextString(m) }
 func (*MaxRunsExceeded) ProtoMessage()    {}
 func (*MaxRunsExceeded) Descriptor() ([]byte, []int) {
-	return fileDescriptor_6aab92ca59e015f8, []int{34}
+	return fileDescriptor_6aab92ca59e015f8, []int{35}
 }
 func (m *MaxRunsExceeded) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -2863,7 +2946,7 @@ func (m *JobDuplicateDetected) Reset()         { *m = JobDuplicateDetected{} }
 func (m *JobDuplicateDetected) String() string { return proto.CompactTextString(m) }
 func (*JobDuplicateDetected) ProtoMessage()    {}
 func (*JobDuplicateDetected) Descriptor() ([]byte, []int) {
-	return fileDescriptor_6aab92ca59e015f8, []int{35}
+	return fileDescriptor_6aab92ca59e015f8, []int{36}
 }
 func (m *JobDuplicateDetected) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -2922,7 +3005,7 @@ func (m *JobRunPreempted) Reset()         { *m = JobRunPreempted{} }
 func (m *JobRunPreempted) String() string { return proto.CompactTextString(m) }
 func (*JobRunPreempted) ProtoMessage()    {}
 func (*JobRunPreempted) Descriptor() ([]byte, []int) {
-	return fileDescriptor_6aab92ca59e015f8, []int{36}
+	return fileDescriptor_6aab92ca59e015f8, []int{37}
 }
 func (m *JobRunPreempted) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -2991,7 +3074,7 @@ func (m *PartitionMarker) Reset()         { *m = PartitionMarker{} }
 func (m *PartitionMarker) String() string { return proto.CompactTextString(m) }
 func (*PartitionMarker) ProtoMessage()    {}
 func (*PartitionMarker) Descriptor() ([]byte, []int) {
-	return fileDescriptor_6aab92ca59e015f8, []int{37}
+	return fileDescriptor_6aab92ca59e015f8, []int{38}
 }
 func (m *PartitionMarker) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -3035,6 +3118,7 @@ func (m *PartitionMarker) GetPartition() uint32 {
 }
 
 func init() {
+	proto.RegisterEnum("armadaevents.JobState", JobState_name, JobState_value)
 	proto.RegisterEnum("armadaevents.KubernetesReason", KubernetesReason_name, KubernetesReason_value)
 	proto.RegisterType((*EventSequence)(nil), "armadaevents.EventSequence")
 	proto.RegisterType((*EventSequence_Event)(nil), "armadaevents.EventSequence.Event")
@@ -3053,6 +3137,7 @@ func init() {
 	proto.RegisterType((*ReprioritiseJobSet)(nil), "armadaevents.ReprioritiseJobSet")
 	proto.RegisterType((*ReprioritisedJob)(nil), "armadaevents.ReprioritisedJob")
 	proto.RegisterType((*CancelJob)(nil), "armadaevents.CancelJob")
+	proto.RegisterType((*JobSetFilter)(nil), "armadaevents.JobSetFilter")
 	proto.RegisterType((*CancelJobSet)(nil), "armadaevents.CancelJobSet")
 	proto.RegisterType((*CancelledJob)(nil), "armadaevents.CancelledJob")
 	proto.RegisterType((*JobSucceeded)(nil), "armadaevents.JobSucceeded")
@@ -3086,201 +3171,206 @@ func init() {
 func init() { proto.RegisterFile("pkg/armadaevents/events.proto", fileDescriptor_6aab92ca59e015f8) }
 
 var fileDescriptor_6aab92ca59e015f8 = []byte{
-	// 3101 bytes of a gzipped FileDescriptorProto
+	// 3179 bytes of a gzipped FileDescriptorProto
 	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe4, 0x5b, 0x4b, 0x6c, 0x1b, 0xc7,
-	0xf9, 0xf7, 0x92, 0x12, 0x1f, 0x9f, 0x1e, 0xa4, 0xc7, 0x92, 0x43, 0x2b, 0x96, 0xe8, 0x6c, 0xf2,
-	0xff, 0xc3, 0x29, 0x12, 0x32, 0x71, 0x1e, 0xc8, 0xa3, 0x48, 0x20, 0xda, 0x6a, 0x6d, 0x47, 0x8a,
-	0x5d, 0xca, 0x4e, 0xd3, 0x20, 0x05, 0xb3, 0xdc, 0x1d, 0x51, 0x6b, 0x2d, 0x77, 0x37, 0xfb, 0x90,
-	0x25, 0x20, 0x87, 0x16, 0x68, 0xd3, 0x5b, 0x9b, 0x43, 0x0f, 0xbd, 0xa5, 0x40, 0x4f, 0x0d, 0xd0,
-	0x73, 0xcf, 0xbd, 0xa5, 0x68, 0x51, 0xa4, 0xb7, 0x9e, 0xd8, 0x22, 0x46, 0x2f, 0x3c, 0xf4, 0xda,
-	0x07, 0x7a, 0x28, 0xe6, 0xb5, 0x3b, 0xb3, 0x5c, 0xda, 0x82, 0x1f, 0x75, 0x0a, 0x9f, 0xa4, 0xfd,
-	0x7d, 0xaf, 0x99, 0xf9, 0xbe, 0xf9, 0xe6, 0x9b, 0x07, 0x61, 0xd5, 0xdf, 0x1b, 0xb4, 0x8d, 0x60,
-	0x68, 0x58, 0x06, 0xde, 0xc7, 0x6e, 0x14, 0xb6, 0xd9, 0x9f, 0x96, 0x1f, 0x78, 0x91, 0x87, 0xe6,
-	0x65, 0xd2, 0x8a, 0xbe, 0xf7, 0x4a, 0xd8, 0xb2, 0xbd, 0xb6, 0xe1, 0xdb, 0x6d, 0xd3, 0x0b, 0x70,
-	0x7b, 0xff, 0xf9, 0xf6, 0x00, 0xbb, 0x38, 0x30, 0x22, 0x6c, 0x31, 0x89, 0x95, 0xb3, 0x12, 0x8f,
-	0x8b, 0xa3, 0x9b, 0x5e, 0xb0, 0x67, 0xbb, 0x83, 0x3c, 0xce, 0xe6, 0xc0, 0xf3, 0x06, 0x0e, 0x6e,
-	0xd3, 0xaf, 0x7e, 0xbc, 0xd3, 0x8e, 0xec, 0x21, 0x0e, 0x23, 0x63, 0xe8, 0x73, 0x86, 0x17, 0x53,
-	0x55, 0x43, 0xc3, 0xdc, 0xb5, 0x5d, 0x1c, 0x1c, 0xb6, 0x69, 0x7b, 0x7d, 0xbb, 0x1d, 0xe0, 0xd0,
-	0x8b, 0x03, 0x13, 0x4f, 0xa8, 0x7d, 0x76, 0x60, 0x47, 0xbb, 0x71, 0xbf, 0x65, 0x7a, 0xc3, 0xf6,
-	0xc0, 0x1b, 0x78, 0xa9, 0x7e, 0xf2, 0x45, 0x3f, 0xe8, 0x7f, 0x8c, 0x5d, 0xff, 0x05, 0x82, 0x85,
-	0x0d, 0xd2, 0xbd, 0x6d, 0xfc, 0x61, 0x8c, 0x5d, 0x13, 0xa3, 0xa7, 0x61, 0xf6, 0xc3, 0x18, 0xc7,
-	0xb8, 0xa1, 0x9d, 0xd1, 0xce, 0x56, 0x3b, 0x27, 0xc6, 0xa3, 0x66, 0x8d, 0x02, 0xcf, 0x78, 0x43,
-	0x3b, 0xc2, 0x43, 0x3f, 0x3a, 0xec, 0x32, 0x0e, 0xf4, 0x1a, 0xcc, 0xdf, 0xf0, 0xfa, 0xbd, 0x10,
-	0x47, 0x3d, 0xd7, 0x18, 0xe2, 0x46, 0x81, 0x4a, 0x34, 0xc6, 0xa3, 0xe6, 0xd2, 0x0d, 0xaf, 0xbf,
-	0x8d, 0xa3, 0xb7, 0x8d, 0xa1, 0x2c, 0x06, 0x29, 0x8a, 0x9e, 0x85, 0x72, 0x1c, 0xe2, 0xa0, 0x67,
-	0x5b, 0x8d, 0x22, 0x15, 0x5b, 0x1a, 0x8f, 0x9a, 0x75, 0x02, 0x5d, 0xb2, 0x24, 0x91, 0x12, 0x43,
-	0xd0, 0x33, 0x50, 0x1a, 0x04, 0x5e, 0xec, 0x87, 0x8d, 0x99, 0x33, 0x45, 0xc1, 0xcd, 0x10, 0x99,
-	0x9b, 0x21, 0xe8, 0x0a, 0x94, 0x98, 0xcf, 0x1a, 0xb3, 0x67, 0x8a, 0x67, 0xe7, 0xce, 0x3d, 0xd1,
-	0x92, 0x1d, 0xd9, 0x52, 0x3a, 0xcc, 0xbe, 0x98, 0x42, 0x46, 0x97, 0x15, 0x72, 0xd7, 0xff, 0xbb,
-	0x06, 0xb3, 0x94, 0x0f, 0x5d, 0x81, 0xb2, 0x19, 0x60, 0x32, 0xe0, 0x0d, 0x74, 0x46, 0x3b, 0x3b,
-	0x77, 0x6e, 0xa5, 0xc5, 0x1c, 0xd9, 0x12, 0x03, 0xdd, 0xba, 0x26, 0x1c, 0xd9, 0x39, 0x35, 0x1e,
-	0x35, 0x8f, 0x73, 0xf6, 0x54, 0xeb, 0x27, 0x7f, 0x6e, 0x6a, 0x5d, 0xa1, 0x05, 0x5d, 0x85, 0x6a,
-	0x18, 0xf7, 0x87, 0x76, 0x74, 0xd9, 0xeb, 0xd3, 0x31, 0x9f, 0x3b, 0xf7, 0x98, 0xda, 0xdc, 0x6d,
-	0x41, 0xee, 0x3c, 0x36, 0x1e, 0x35, 0x4f, 0x24, 0xdc, 0xa9, 0xc6, 0x8b, 0xc7, 0xba, 0xa9, 0x12,
-	0xb4, 0x0b, 0xb5, 0x00, 0xfb, 0x81, 0xed, 0x05, 0x76, 0x64, 0x87, 0x98, 0xe8, 0x2d, 0x50, 0xbd,
-	0xab, 0xaa, 0xde, 0xae, 0xca, 0xd4, 0x59, 0x1d, 0x8f, 0x9a, 0xa7, 0x32, 0x92, 0x8a, 0x8d, 0xac,
-	0x5a, 0x14, 0x01, 0xca, 0x40, 0xdb, 0x38, 0xa2, 0xfe, 0x9c, 0x3b, 0x77, 0xe6, 0xb6, 0xc6, 0xb6,
-	0x71, 0xd4, 0x39, 0x33, 0x1e, 0x35, 0x4f, 0x4f, 0xca, 0x2b, 0x26, 0x73, 0xf4, 0x23, 0x07, 0xea,
-	0x32, 0x6a, 0x91, 0x0e, 0xce, 0x50, 0x9b, 0x6b, 0xd3, 0x6d, 0x12, 0xae, 0xce, 0xda, 0x78, 0xd4,
-	0x5c, 0xc9, 0xca, 0x2a, 0xf6, 0x26, 0x34, 0x13, 0xff, 0x98, 0x86, 0x6b, 0x62, 0x87, 0x98, 0x99,
-	0xcd, 0xf3, 0xcf, 0x79, 0x41, 0x66, 0xfe, 0x49, 0xb8, 0x55, 0xff, 0x24, 0x30, 0x7a, 0x1f, 0xe6,
-	0x93, 0x0f, 0x32, 0x5e, 0x25, 0x1e, 0x47, 0xf9, 0x4a, 0xc9, 0x48, 0xad, 0x8c, 0x47, 0xcd, 0x93,
-	0xb2, 0x8c, 0xa2, 0x5a, 0xd1, 0x96, 0x6a, 0x77, 0xd8, 0xc8, 0x94, 0xa7, 0x6b, 0x67, 0x1c, 0xb2,
-	0x76, 0x67, 0x72, 0x44, 0x14, 0x6d, 0x44, 0x3b, 0x99, 0xc4, 0xb1, 0x69, 0x62, 0x6c, 0x61, 0xab,
-	0x51, 0xc9, 0xd3, 0x7e, 0x59, 0xe2, 0x60, 0xda, 0x65, 0x19, 0x55, 0xbb, 0x4c, 0x21, 0x63, 0x7d,
-	0xc3, 0xeb, 0x6f, 0x04, 0x81, 0x17, 0x84, 0x8d, 0x6a, 0xde, 0x58, 0x5f, 0x16, 0x64, 0x36, 0xd6,
-	0x09, 0xb7, 0x3a, 0xd6, 0x09, 0xcc, 0xdb, 0xdb, 0x8d, 0xdd, 0x4d, 0x6c, 0x84, 0xd8, 0x6a, 0xc0,
-	0x94, 0xf6, 0x26, 0x1c, 0x49, 0x7b, 0x13, 0x64, 0xa2, 0xbd, 0x09, 0x05, 0x59, 0xb0, 0xc8, 0xbe,
-	0xd7, 0xc3, 0xd0, 0x1e, 0xb8, 0xd8, 0x6a, 0xcc, 0x51, 0xfd, 0xa7, 0xf3, 0xf4, 0x0b, 0x9e, 0xce,
-	0xe9, 0xf1, 0xa8, 0xd9, 0x50, 0xe5, 0x14, 0x1b, 0x19, 0x9d, 0xe8, 0x03, 0x58, 0x60, 0x48, 0x37,
-	0x76, 0x5d, 0xdb, 0x1d, 0x34, 0xe6, 0xa9, 0x91, 0xc7, 0xf3, 0x8c, 0x70, 0x96, 0xce, 0xe3, 0xe3,
-	0x51, 0xf3, 0x31, 0x45, 0x4a, 0x31, 0xa1, 0x2a, 0x24, 0x19, 0x83, 0x01, 0xa9, 0x63, 0x17, 0xf2,
-	0x32, 0xc6, 0x65, 0x95, 0x89, 0x65, 0x8c, 0x8c, 0xa4, 0x9a, 0x31, 0x32, 0xc4, 0xd4, 0x1f, 0xdc,
-	0xc9, 0x8b, 0xd3, 0xfd, 0xc1, 0xfd, 0x2c, 0xf9, 0x23, 0xc7, 0xd5, 0x8a, 0x36, 0xf4, 0x11, 0x90,
-	0x85, 0xe7, 0x42, 0xec, 0x3b, 0xb6, 0x69, 0x44, 0xf8, 0x02, 0x8e, 0xb0, 0x49, 0x32, 0x75, 0x8d,
-	0x5a, 0xd1, 0x27, 0xac, 0x4c, 0x70, 0x76, 0xf4, 0xf1, 0xa8, 0xb9, 0x96, 0xa7, 0x43, 0xb1, 0x9a,
-	0x6b, 0x05, 0x7d, 0x4f, 0x83, 0xe5, 0x30, 0x32, 0x5c, 0xcb, 0x70, 0x3c, 0x17, 0x5f, 0x72, 0x07,
-	0x01, 0x0e, 0xc3, 0x4b, 0xee, 0x8e, 0xd7, 0xa8, 0x53, 0xfb, 0x4f, 0x66, 0xd2, 0x7a, 0x1e, 0x6b,
-	0xe7, 0xc9, 0xf1, 0xa8, 0xd9, 0xcc, 0xd5, 0xa2, 0xb4, 0x20, 0xdf, 0x10, 0x3a, 0x80, 0x13, 0xa2,
-	0x32, 0xb8, 0x1e, 0xd9, 0x8e, 0x1d, 0x1a, 0x91, 0xed, 0xb9, 0x8d, 0xe3, 0xd4, 0xfe, 0x13, 0xd9,
-	0xec, 0x38, 0xc1, 0xd8, 0x79, 0x62, 0x3c, 0x6a, 0xae, 0xe6, 0x68, 0x50, 0x6c, 0xe7, 0x99, 0x48,
-	0x43, 0xe8, 0x6a, 0x80, 0x09, 0x23, 0xb6, 0x1a, 0x27, 0xa6, 0x87, 0x50, 0xc2, 0x24, 0x87, 0x50,
-	0x02, 0xe6, 0x85, 0x50, 0x42, 0x24, 0x96, 0x7c, 0x23, 0x88, 0x6c, 0x62, 0x76, 0xcb, 0x08, 0xf6,
-	0x70, 0xd0, 0x58, 0xca, 0xb3, 0x74, 0x55, 0x65, 0x62, 0x96, 0x32, 0x92, 0xaa, 0xa5, 0x0c, 0xb1,
-	0x53, 0x86, 0x59, 0xaa, 0x4b, 0x1f, 0x97, 0xe0, 0x44, 0xce, 0x70, 0xa1, 0x37, 0xa0, 0x14, 0xc4,
-	0x2e, 0xa9, 0x61, 0xd8, 0xc2, 0x8d, 0xd4, 0x16, 0x5c, 0x8f, 0x6d, 0x8b, 0x15, 0x50, 0x41, 0xec,
-	0x2a, 0x65, 0xcd, 0x2c, 0x05, 0x88, 0x3c, 0x29, 0xa0, 0x6c, 0x8b, 0x2f, 0xd0, 0x53, 0xe5, 0x6f,
-	0x78, 0x7d, 0x55, 0x9e, 0x02, 0x08, 0xc3, 0x82, 0xf0, 0x45, 0xcf, 0x26, 0x81, 0xc6, 0x96, 0xde,
-	0xa7, 0x54, 0x35, 0x6f, 0xc5, 0x7d, 0x1c, 0xb8, 0x38, 0xc2, 0xa1, 0xe8, 0x03, 0x8d, 0x34, 0x3a,
-	0xb1, 0x02, 0x09, 0x91, 0xf4, 0xcf, 0xcb, 0x38, 0xfa, 0xa9, 0x06, 0x8d, 0xa1, 0x71, 0xd0, 0x13,
-	0x60, 0xd8, 0xdb, 0xf1, 0x82, 0x9e, 0x8f, 0x03, 0xdb, 0xb3, 0x68, 0x3d, 0x36, 0x77, 0xee, 0xeb,
-	0x77, 0x8c, 0xad, 0xd6, 0x96, 0x71, 0x20, 0xe0, 0xf0, 0x1b, 0x5e, 0x70, 0x95, 0x8a, 0x6f, 0xb8,
-	0x51, 0x70, 0xd8, 0x59, 0xfd, 0x7c, 0xd4, 0x3c, 0x36, 0x1e, 0x35, 0x97, 0x87, 0x79, 0x3c, 0xdd,
-	0x7c, 0x18, 0xfd, 0x44, 0x83, 0x93, 0x91, 0x17, 0x19, 0x4e, 0xcf, 0x8c, 0x87, 0xb1, 0x63, 0x44,
-	0xf6, 0x3e, 0xee, 0xc5, 0xa1, 0x31, 0xc0, 0xbc, 0xec, 0x7b, 0xfd, 0xce, 0x8d, 0xba, 0x46, 0xe4,
-	0xcf, 0x27, 0xe2, 0xd7, 0x89, 0x34, 0x6b, 0xd3, 0x69, 0xde, 0xa6, 0xa5, 0x28, 0x87, 0xa5, 0x9b,
-	0x8b, 0xae, 0xfc, 0x5c, 0x83, 0x95, 0xe9, 0xdd, 0x44, 0x4f, 0x42, 0x71, 0x0f, 0x1f, 0xf2, 0xc2,
-	0xfa, 0xf8, 0x78, 0xd4, 0x5c, 0xd8, 0xc3, 0x87, 0xd2, 0xa8, 0x13, 0x2a, 0xfa, 0x0e, 0xcc, 0xee,
-	0x1b, 0x4e, 0x8c, 0x79, 0x48, 0xb4, 0x5a, 0x6c, 0x1b, 0xd0, 0x92, 0xb7, 0x01, 0x2d, 0x7f, 0x6f,
-	0x40, 0x80, 0x96, 0xf0, 0x48, 0xeb, 0x5b, 0xb1, 0xe1, 0x46, 0x76, 0x74, 0xc8, 0xc2, 0x85, 0x2a,
-	0x90, 0xc3, 0x85, 0x02, 0xaf, 0x15, 0x5e, 0xd1, 0x56, 0x3e, 0xd5, 0xe0, 0xd4, 0xd4, 0x4e, 0x7f,
-	0x15, 0x5a, 0xa8, 0xf7, 0x60, 0x86, 0x04, 0x3e, 0x29, 0xf9, 0x77, 0xed, 0xc1, 0xee, 0xcb, 0x2f,
-	0xd2, 0xe6, 0x94, 0x58, 0x85, 0xce, 0x10, 0xb9, 0x42, 0x67, 0x08, 0xd9, 0xb6, 0x38, 0xde, 0xcd,
-	0x97, 0x5f, 0xa4, 0x8d, 0x2a, 0x31, 0x23, 0x14, 0x90, 0x8d, 0x50, 0x40, 0xff, 0xfb, 0x2c, 0x54,
-	0x93, 0x9a, 0x5a, 0x9a, 0x83, 0xda, 0x5d, 0xcd, 0xc1, 0x8b, 0x50, 0xb7, 0xb0, 0xc5, 0x17, 0x03,
-	0xdb, 0x73, 0xc5, 0x6c, 0xae, 0xb2, 0x84, 0xa3, 0xd0, 0x14, 0xf9, 0x5a, 0x86, 0x84, 0xce, 0x41,
-	0x85, 0xd7, 0x9e, 0x87, 0x74, 0x22, 0x2f, 0x74, 0x4e, 0x8e, 0x47, 0x4d, 0x24, 0x30, 0x49, 0x34,
-	0xe1, 0x43, 0x5d, 0x00, 0xaf, 0x7f, 0x03, 0x9b, 0xd1, 0x16, 0x8e, 0x0c, 0x5e, 0x05, 0x37, 0xd4,
-	0x1e, 0x5c, 0x49, 0xe8, 0x6c, 0x6b, 0x96, 0xf2, 0xcb, 0x5b, 0xb3, 0x14, 0x45, 0xef, 0x03, 0x0c,
-	0x0d, 0xdb, 0x65, 0x72, 0xbc, 0xe4, 0xd5, 0xa7, 0xa5, 0x94, 0xad, 0x84, 0x93, 0x69, 0x4f, 0x25,
-	0x65, 0xed, 0x29, 0x4a, 0x36, 0x50, 0xcc, 0x56, 0xd8, 0x28, 0xd1, 0x59, 0xba, 0x36, 0x4d, 0x35,
-	0x57, 0xbb, 0x4c, 0x36, 0x51, 0x5c, 0x44, 0xd2, 0x29, 0xb4, 0x90, 0x61, 0x73, 0xec, 0x1d, 0x4c,
-	0xb6, 0xcf, 0xb4, 0xd8, 0xe5, 0xc3, 0x26, 0x30, 0x79, 0xd8, 0x04, 0x86, 0x5e, 0x01, 0x30, 0xa2,
-	0x2d, 0x2f, 0x8c, 0xae, 0xb8, 0x26, 0xa6, 0x45, 0x6c, 0x85, 0x35, 0x3f, 0x45, 0xe5, 0xe6, 0xa7,
-	0x28, 0x7a, 0x1d, 0xe6, 0x7c, 0xb6, 0x14, 0xd9, 0x7d, 0x07, 0xd3, 0x22, 0xb5, 0x42, 0xf7, 0x79,
-	0xcb, 0x12, 0x2c, 0xc9, 0xca, 0xdc, 0xe8, 0x9b, 0x50, 0x33, 0x3d, 0xd7, 0x8c, 0x83, 0x00, 0xbb,
-	0xe6, 0xe1, 0xb6, 0xb1, 0x83, 0x69, 0x41, 0x5a, 0x61, 0xa1, 0x92, 0x21, 0xc9, 0xa1, 0x92, 0x21,
-	0xa1, 0x97, 0xa0, 0x1a, 0x9a, 0xbb, 0xd8, 0x8a, 0x1d, 0x1c, 0xd0, 0x9a, 0xb3, 0xca, 0xf7, 0x86,
-	0x02, 0x94, 0x84, 0x53, 0x4e, 0xfd, 0xf7, 0x1a, 0x2c, 0xe5, 0xb9, 0x2e, 0x13, 0x46, 0xda, 0x7d,
-	0x09, 0xa3, 0x77, 0xa0, 0xe2, 0x7b, 0x56, 0x2f, 0xf4, 0xb1, 0xc9, 0x33, 0x45, 0x26, 0x88, 0xae,
-	0x7a, 0xd6, 0xb6, 0x8f, 0xcd, 0x6f, 0xdb, 0xd1, 0xee, 0xfa, 0xbe, 0x67, 0x5b, 0x9b, 0x76, 0xc8,
-	0xbd, 0xed, 0x33, 0x8a, 0xb2, 0x3a, 0x97, 0x39, 0xd8, 0xa9, 0x40, 0x89, 0x59, 0xd1, 0xff, 0x50,
-	0x84, 0x7a, 0x36, 0x5c, 0xfe, 0x97, 0xba, 0x82, 0xde, 0x85, 0xb2, 0xcd, 0xaa, 0x37, 0xbe, 0x72,
-	0xff, 0x9f, 0x94, 0x4b, 0x5b, 0xe9, 0xf9, 0x51, 0x6b, 0xff, 0xf9, 0x16, 0x2f, 0xf3, 0xe8, 0x10,
-	0x50, 0xcd, 0x5c, 0x52, 0xd5, 0xcc, 0x41, 0xd4, 0x85, 0x72, 0x88, 0x83, 0x7d, 0xdb, 0xc4, 0x3c,
-	0x29, 0x34, 0x65, 0xcd, 0xa6, 0x17, 0x60, 0xa2, 0x73, 0x9b, 0xb1, 0xa4, 0x3a, 0xb9, 0x8c, 0xaa,
-	0x93, 0x83, 0xe8, 0x1d, 0xa8, 0x9a, 0x9e, 0xbb, 0x63, 0x0f, 0xb6, 0x0c, 0x9f, 0xa7, 0x85, 0xd5,
-	0x3c, 0xad, 0xe7, 0x05, 0x13, 0xdf, 0x0f, 0x8b, 0xcf, 0xcc, 0x7e, 0x38, 0xe1, 0x4a, 0x1d, 0xfa,
-	0xb7, 0x19, 0x80, 0xd4, 0x39, 0xe8, 0x55, 0x98, 0xc3, 0x07, 0xd8, 0x8c, 0x23, 0x2f, 0x10, 0xf9,
-	0x99, 0x1f, 0x2f, 0x09, 0x58, 0x49, 0xa8, 0x90, 0xa2, 0x64, 0x82, 0xb8, 0xc6, 0x10, 0x87, 0xbe,
-	0x61, 0x8a, 0x73, 0x29, 0xda, 0x98, 0x04, 0x94, 0x27, 0x48, 0x02, 0xa2, 0xff, 0x87, 0x19, 0x7a,
-	0x92, 0xc5, 0x8e, 0xa4, 0xd0, 0x78, 0xd4, 0x5c, 0x74, 0xd5, 0x33, 0x2c, 0x4a, 0x47, 0x6f, 0xc2,
-	0xc2, 0x5e, 0x12, 0x78, 0xa4, 0x6d, 0x33, 0x54, 0x80, 0x96, 0x54, 0x29, 0x41, 0x69, 0xdd, 0xbc,
-	0x8c, 0xa3, 0x1d, 0x98, 0x33, 0x5c, 0xd7, 0x8b, 0x68, 0xee, 0x17, 0xc7, 0x54, 0x4f, 0x4f, 0x0b,
-	0xd3, 0xd6, 0x7a, 0xca, 0xcb, 0xaa, 0x13, 0x9a, 0x71, 0x24, 0x0d, 0x72, 0xc6, 0x91, 0x60, 0xd4,
-	0x85, 0x92, 0x63, 0xf4, 0xb1, 0x23, 0x92, 0xed, 0x53, 0x53, 0x4d, 0x6c, 0x52, 0x36, 0xa6, 0x9d,
-	0x2e, 0xb5, 0x4c, 0x4e, 0x5e, 0x6a, 0x19, 0xb2, 0xb2, 0x03, 0xf5, 0x6c, 0x7b, 0x8e, 0x56, 0x38,
-	0x3c, 0x2d, 0x17, 0x0e, 0xd5, 0x3b, 0x96, 0x2a, 0x06, 0xcc, 0x49, 0x8d, 0x7a, 0x10, 0x26, 0xf4,
-	0x5f, 0x6a, 0xb0, 0x94, 0x37, 0x77, 0xd1, 0x96, 0x34, 0xe3, 0x35, 0xbe, 0xdd, 0xce, 0x09, 0x75,
-	0x2e, 0x3b, 0x65, 0xaa, 0xa7, 0x13, 0xbd, 0x03, 0x8b, 0xae, 0x67, 0xe1, 0x9e, 0x41, 0x0c, 0x38,
-	0x76, 0x18, 0x35, 0x0a, 0xf4, 0x18, 0x93, 0x6e, 0xd3, 0x09, 0x65, 0x5d, 0x10, 0x24, 0xe9, 0x05,
-	0x85, 0xa0, 0xff, 0x50, 0x83, 0x5a, 0xe6, 0x14, 0xed, 0x9e, 0x8b, 0x17, 0xb9, 0xe4, 0x28, 0x1c,
-	0xad, 0xe4, 0xd0, 0x2f, 0x02, 0x9a, 0x3c, 0xcc, 0x53, 0x34, 0x69, 0x47, 0xd4, 0xf4, 0xb1, 0x06,
-	0xf5, 0xec, 0x19, 0xdd, 0x43, 0xe9, 0xd2, 0x5b, 0x50, 0x4d, 0xce, 0xdb, 0xee, 0xb5, 0x01, 0xfa,
-	0x22, 0xcc, 0xcb, 0x87, 0x77, 0xfa, 0xdb, 0xe2, 0xdb, 0xb9, 0x2f, 0x1d, 0xd4, 0x7f, 0xad, 0xc1,
-	0xbc, 0x7c, 0xc2, 0x76, 0xcf, 0x23, 0x36, 0x48, 0x77, 0x91, 0x64, 0xbb, 0x17, 0xd2, 0xd8, 0x3c,
-	0xea, 0x2e, 0x92, 0x46, 0xb0, 0x22, 0x2e, 0x47, 0xb0, 0x42, 0xd0, 0x7f, 0xcb, 0x5a, 0x9e, 0x9e,
-	0x9f, 0x3d, 0xec, 0xfd, 0x73, 0x66, 0x81, 0x29, 0x1e, 0x7d, 0x81, 0xd1, 0xff, 0xa9, 0xc1, 0xa2,
-	0x7a, 0xae, 0xf7, 0xd0, 0x7b, 0x33, 0xe1, 0xc7, 0xe2, 0x03, 0xf2, 0xe3, 0x3f, 0x34, 0x58, 0x50,
-	0x8e, 0x1b, 0x1f, 0x9d, 0xae, 0xff, 0xac, 0x00, 0x27, 0xf3, 0xd5, 0x3c, 0x90, 0xc2, 0xf3, 0x22,
-	0x90, 0x25, 0x84, 0x9e, 0x21, 0xb2, 0x81, 0x59, 0x9e, 0xa8, 0x3b, 0x69, 0x17, 0xc4, 0xfa, 0x33,
-	0x71, 0x4e, 0x28, 0xc4, 0xd1, 0xbb, 0x30, 0x67, 0x4b, 0x27, 0x92, 0xac, 0xdc, 0x3c, 0xa5, 0x6a,
-	0x93, 0xcf, 0x21, 0x69, 0x81, 0x61, 0x4f, 0x39, 0x7d, 0x94, 0x55, 0x75, 0x4a, 0x30, 0x63, 0xbb,
-	0x3b, 0x9e, 0xbe, 0x0f, 0x65, 0xde, 0x1c, 0xf4, 0x02, 0x54, 0xe9, 0x72, 0x47, 0x6b, 0x29, 0xb6,
-	0x60, 0xd3, 0x24, 0x4c, 0xc0, 0xcc, 0x9d, 0x60, 0x45, 0x60, 0xe8, 0x65, 0x00, 0xb2, 0xe4, 0xba,
-	0xf1, 0xb0, 0x8f, 0x03, 0xda, 0xdd, 0x59, 0x56, 0xb3, 0xf9, 0x9e, 0xf5, 0x36, 0x05, 0xe5, 0x9a,
-	0x2d, 0x01, 0xf5, 0x5f, 0x15, 0x60, 0x4e, 0x3e, 0x03, 0xbd, 0x2b, 0xe3, 0x1f, 0x81, 0xa8, 0xa7,
-	0x7b, 0x86, 0x65, 0x91, 0xbf, 0x58, 0xe4, 0xc1, 0xf6, 0xd4, 0x41, 0x12, 0xff, 0xaf, 0x0b, 0x09,
-	0x56, 0x3d, 0xd1, 0x5b, 0x26, 0x3b, 0x43, 0x92, 0xac, 0xd6, 0xb3, 0xb4, 0x95, 0x3d, 0x58, 0xce,
-	0x55, 0x25, 0xd7, 0x3c, 0xb3, 0xf7, 0xab, 0xe6, 0xf9, 0xcd, 0x2c, 0x2c, 0xe7, 0x9e, 0x3d, 0x3f,
-	0xf4, 0x59, 0xac, 0xce, 0xa0, 0xe2, 0x7d, 0x99, 0x41, 0x1f, 0x6b, 0x79, 0x9e, 0x65, 0x87, 0x96,
-	0xaf, 0x1e, 0xe1, 0x40, 0xfe, 0x7e, 0xf9, 0x58, 0x0d, 0xcb, 0xd9, 0xbb, 0x9a, 0x13, 0xa5, 0xa3,
-	0xce, 0x09, 0xf4, 0x1c, 0x2b, 0x5f, 0xa9, 0xad, 0x32, 0xb5, 0x25, 0x32, 0x44, 0xc6, 0x54, 0x99,
-	0x43, 0x64, 0x47, 0x23, 0x24, 0xd8, 0xa6, 0xa9, 0x92, 0xee, 0x68, 0x38, 0x4f, 0x76, 0xdf, 0x34,
-	0x2f, 0xe3, 0xff, 0xdd, 0x18, 0xfe, 0x97, 0x06, 0xb5, 0xcc, 0x65, 0xd4, 0xa3, 0xb3, 0x06, 0xfd,
-	0x58, 0x83, 0x6a, 0x72, 0x0f, 0x7a, 0xcf, 0xd5, 0xdf, 0x3a, 0x94, 0x30, 0xbb, 0x8b, 0x63, 0xe9,
-	0xee, 0x44, 0xe6, 0xad, 0x04, 0xa1, 0xf1, 0xd7, 0x11, 0x99, 0xeb, 0xb7, 0x2e, 0x17, 0xd4, 0xff,
-	0x98, 0xd4, 0x75, 0x69, 0x9b, 0x1e, 0xaa, 0x2b, 0xd2, 0x3e, 0x15, 0xef, 0xb6, 0x4f, 0xbf, 0x2b,
-	0xc3, 0x2c, 0xe5, 0x23, 0x1b, 0x8a, 0x08, 0x07, 0x43, 0xdb, 0x35, 0x1c, 0xda, 0x9d, 0x0a, 0x9b,
-	0xb7, 0x02, 0x93, 0xe7, 0xad, 0xc0, 0xd0, 0x2e, 0xd4, 0xd2, 0xed, 0x3e, 0x55, 0x93, 0xff, 0x04,
-	0xe3, 0x2d, 0x95, 0x89, 0x9d, 0x03, 0x66, 0x24, 0xd5, 0x3b, 0xaa, 0x0c, 0x11, 0x59, 0xb0, 0x68,
-	0x7a, 0x6e, 0x64, 0xd8, 0x2e, 0x0e, 0x98, 0xa1, 0x62, 0xde, 0x15, 0xf4, 0x79, 0x85, 0x87, 0x5d,
-	0x41, 0xab, 0x72, 0xea, 0x15, 0xb4, 0x4a, 0x43, 0x1f, 0xc0, 0x82, 0xa8, 0x7d, 0x99, 0x91, 0x99,
-	0xbc, 0x2b, 0xe8, 0x0d, 0x99, 0x85, 0x85, 0xb4, 0x22, 0xa5, 0x5e, 0x41, 0x2b, 0x24, 0xe4, 0x40,
-	0xdd, 0xf7, 0xac, 0xeb, 0x2e, 0x3f, 0xac, 0x34, 0xfa, 0x0e, 0xe6, 0x67, 0x4c, 0x6b, 0x13, 0x25,
-	0x8f, 0xc2, 0xc5, 0x52, 0x71, 0x56, 0x56, 0x7d, 0xd4, 0x91, 0xa5, 0xa2, 0xf7, 0x61, 0xde, 0x21,
-	0x5b, 0x90, 0x8d, 0x03, 0xdf, 0x0e, 0xb0, 0x95, 0xff, 0x04, 0x63, 0x53, 0xe2, 0x60, 0x89, 0x50,
-	0x96, 0x51, 0xaf, 0xa1, 0x65, 0x0a, 0xf1, 0xfe, 0xd0, 0x38, 0xe8, 0xc6, 0x6e, 0xb8, 0x71, 0xc0,
-	0xaf, 0xd3, 0xcb, 0x79, 0xde, 0xdf, 0x52, 0x99, 0x98, 0xf7, 0x33, 0x92, 0xaa, 0xf7, 0x33, 0x44,
-	0xb4, 0x49, 0xf3, 0x3c, 0x73, 0x09, 0x7b, 0x8a, 0x71, 0x72, 0x62, 0xb4, 0x98, 0x37, 0xd8, 0x26,
-	0x98, 0x7f, 0x29, 0x4a, 0x13, 0x0d, 0xdc, 0x07, 0xb4, 0xdb, 0x5d, 0x1c, 0xc5, 0x81, 0x8b, 0x2d,
-	0xfe, 0x0a, 0x63, 0xd2, 0x07, 0x0a, 0x57, 0xe2, 0x03, 0x05, 0x9d, 0xf0, 0x81, 0x42, 0x25, 0x31,
-	0xe5, 0x7b, 0xd6, 0x35, 0x36, 0x65, 0xa2, 0xe4, 0x6d, 0xc6, 0xe3, 0x13, 0xa6, 0x52, 0x16, 0x16,
-	0x53, 0x8a, 0x94, 0x1a, 0x53, 0x0a, 0xa9, 0x53, 0x81, 0x52, 0x80, 0x8d, 0xd0, 0x73, 0xf5, 0x4f,
-	0x35, 0xa8, 0x65, 0xe6, 0x1a, 0x7a, 0x03, 0x92, 0x5b, 0xce, 0x6b, 0x87, 0xbe, 0x28, 0x15, 0x95,
-	0x5b, 0x51, 0x82, 0xe7, 0xdd, 0x8a, 0x12, 0x1c, 0x6d, 0x02, 0x24, 0x79, 0xf9, 0x76, 0x89, 0x8a,
-	0xd6, 0x29, 0x29, 0xa7, 0x5c, 0xa7, 0xa4, 0xa8, 0xfe, 0x45, 0x11, 0x2a, 0xc2, 0x59, 0x0f, 0x64,
-	0x2b, 0xd1, 0x86, 0xf2, 0x10, 0x87, 0xf4, 0x76, 0xb4, 0x90, 0x56, 0x04, 0x1c, 0x92, 0x2b, 0x02,
-	0x0e, 0xa9, 0x05, 0x4b, 0xf1, 0xae, 0x0a, 0x96, 0x99, 0x23, 0x17, 0x2c, 0x98, 0xde, 0x8c, 0x48,
-	0x29, 0x47, 0x9c, 0x89, 0xde, 0x3e, 0x8f, 0x89, 0x7b, 0x13, 0x59, 0x30, 0x73, 0x6f, 0x22, 0x93,
-	0xd0, 0x1e, 0x1c, 0x97, 0xce, 0x6d, 0x59, 0x70, 0xd0, 0xc9, 0xbf, 0x38, 0xfd, 0x1a, 0xaa, 0x4b,
-	0xb9, 0x58, 0x88, 0xef, 0x65, 0x50, 0xb9, 0xe2, 0xcb, 0xd2, 0xf4, 0xbf, 0x16, 0x60, 0x51, 0x6d,
-	0xef, 0x03, 0x71, 0xec, 0x0b, 0x50, 0xc5, 0x07, 0x76, 0xd4, 0x33, 0x3d, 0x0b, 0xf3, 0x6d, 0x13,
-	0xf5, 0x13, 0x01, 0xcf, 0x7b, 0x96, 0xe2, 0x27, 0x81, 0xc9, 0xd1, 0x50, 0x3c, 0x52, 0x34, 0x3c,
-	0x23, 0xe6, 0x12, 0x3f, 0xea, 0xa6, 0xab, 0x67, 0x90, 0x1d, 0x04, 0xce, 0x93, 0x3f, 0xce, 0xd5,
-	0x07, 0x34, 0xce, 0x9f, 0x14, 0xa0, 0x9e, 0xcd, 0x48, 0x5f, 0x8d, 0x29, 0xa4, 0xce, 0x86, 0xe2,
-	0x91, 0x67, 0xc3, 0x9b, 0xb0, 0x40, 0xea, 0x27, 0x23, 0x8a, 0xf8, 0x53, 0x9a, 0x19, 0x5a, 0x77,
-	0xb0, 0xdc, 0x14, 0xbb, 0xeb, 0x02, 0x57, 0x72, 0x93, 0x84, 0xeb, 0xdf, 0x2f, 0xc0, 0x82, 0x92,
-	0x39, 0x1f, 0xbd, 0x94, 0xa2, 0xd7, 0x60, 0x41, 0x29, 0x48, 0xf4, 0x1f, 0xb0, 0x38, 0x51, 0x2b,
-	0x81, 0x47, 0x6f, 0x5c, 0x16, 0x61, 0x5e, 0xae, 0x6c, 0xf4, 0xe3, 0x50, 0xcb, 0x14, 0x22, 0xfa,
-	0x67, 0x1a, 0x2c, 0xe5, 0x3d, 0x8f, 0x43, 0x17, 0x01, 0x5c, 0x7c, 0xb3, 0x77, 0xc7, 0x0d, 0x07,
-	0x6b, 0x3d, 0xbe, 0x79, 0x39, 0x53, 0x9f, 0x57, 0x04, 0x46, 0x34, 0x79, 0x8e, 0xd5, 0xbb, 0x63,
-	0x99, 0x4f, 0x35, 0x79, 0x8e, 0x35, 0xa1, 0x49, 0x60, 0xfa, 0x8f, 0x8a, 0x62, 0x2f, 0x98, 0xbe,
-	0x11, 0x7b, 0x0f, 0xea, 0xbe, 0xf8, 0xb8, 0x73, 0x6b, 0x69, 0x35, 0x9c, 0xf0, 0x67, 0x2d, 0x2d,
-	0xaa, 0x14, 0x55, 0x37, 0xdf, 0xe6, 0x14, 0x8e, 0xa8, 0xbb, 0x9b, 0xd9, 0xef, 0x2c, 0xaa, 0x14,
-	0xf4, 0x5d, 0x38, 0x2e, 0xde, 0x0b, 0xec, 0x63, 0xd1, 0xf0, 0xe2, 0x54, 0xe5, 0xec, 0x49, 0x5b,
-	0x22, 0x90, 0x6d, 0x79, 0x2d, 0x43, 0xca, 0xa8, 0xe7, 0x6d, 0x9f, 0x39, 0xaa, 0xfa, 0x6c, 0xe3,
-	0x6b, 0x19, 0x12, 0xd9, 0x98, 0xd6, 0x32, 0xaf, 0xee, 0xd0, 0x05, 0xa8, 0xd0, 0x47, 0xf9, 0xb7,
-	0xf7, 0x00, 0x9d, 0x20, 0x94, 0x4f, 0xb1, 0x50, 0xe6, 0x10, 0x7a, 0x09, 0xaa, 0xc9, 0xe3, 0x3c,
-	0x7e, 0xab, 0xc3, 0x42, 0x5d, 0x80, 0x4a, 0xa8, 0x0b, 0xf0, 0x6b, 0x9b, 0xf2, 0xfb, 0x00, 0xb6,
-	0x5a, 0xa0, 0x79, 0xa8, 0xac, 0xfb, 0x3e, 0xcd, 0x08, 0xf5, 0x63, 0x68, 0x0e, 0xca, 0x1b, 0xfb,
-	0x36, 0x89, 0xed, 0xba, 0x86, 0xca, 0x50, 0xbc, 0x72, 0x65, 0xab, 0x5e, 0x40, 0x4b, 0x50, 0xbf,
-	0x80, 0x0d, 0xcb, 0xb1, 0x5d, 0x2c, 0xe6, 0x44, 0xbd, 0xd8, 0xb9, 0xf1, 0xf9, 0x97, 0x6b, 0xda,
-	0x17, 0x5f, 0xae, 0x69, 0x7f, 0xf9, 0x72, 0x4d, 0xfb, 0xe4, 0xd6, 0xda, 0xb1, 0x2f, 0x6e, 0xad,
-	0x1d, 0xfb, 0xd3, 0xad, 0xb5, 0x63, 0xef, 0x3d, 0x27, 0xfd, 0xe8, 0x82, 0x75, 0xce, 0x0f, 0x3c,
-	0x92, 0x0e, 0xf8, 0x57, 0x3b, 0xfb, 0x33, 0x93, 0xcf, 0x0a, 0xab, 0xeb, 0xf4, 0xf3, 0x2a, 0xe3,
-	0x6b, 0x5d, 0xf2, 0x5a, 0x0c, 0xa0, 0xbf, 0x32, 0x08, 0xfb, 0x25, 0xfa, 0x6b, 0x82, 0x17, 0xfe,
-	0x13, 0x00, 0x00, 0xff, 0xff, 0x6d, 0x7a, 0x9d, 0x7f, 0xa1, 0x32, 0x00, 0x00,
+	0xf9, 0xf7, 0x92, 0x12, 0x1f, 0x9f, 0x1e, 0xa4, 0xc7, 0xb2, 0x43, 0x2b, 0xb6, 0xe8, 0x6c, 0xf2,
+	0xff, 0xc3, 0x09, 0x12, 0x32, 0x51, 0x1e, 0xc8, 0xa3, 0x48, 0x20, 0x5a, 0x4a, 0x6c, 0x47, 0x92,
+	0x15, 0xca, 0x4a, 0xd3, 0x20, 0x05, 0xb3, 0xdc, 0x1d, 0x51, 0x6b, 0x91, 0xbb, 0x9b, 0x7d, 0xc8,
+	0x12, 0x90, 0x43, 0x0b, 0xb4, 0xe9, 0xad, 0xcd, 0xa1, 0x87, 0xde, 0x52, 0xa0, 0xa7, 0x06, 0xe8,
+	0xb9, 0xe7, 0xde, 0x52, 0xb4, 0x28, 0xd2, 0x5b, 0x4f, 0x6c, 0x91, 0xa0, 0x17, 0x1e, 0x7a, 0xed,
+	0x03, 0x3d, 0x14, 0xf3, 0xda, 0x9d, 0x59, 0x2e, 0x6d, 0xc1, 0x8f, 0x3a, 0x85, 0x4f, 0xd2, 0xfe,
+	0xbe, 0xef, 0xfb, 0x7d, 0xb3, 0xf3, 0xcd, 0x7c, 0xf3, 0xcd, 0xcc, 0x12, 0xce, 0x7b, 0xfb, 0xbd,
+	0xa6, 0xe1, 0x0f, 0x0c, 0xcb, 0xc0, 0x07, 0xd8, 0x09, 0x83, 0x26, 0xfb, 0xd3, 0xf0, 0x7c, 0x37,
+	0x74, 0xd1, 0xac, 0x2c, 0x5a, 0xd4, 0xf7, 0x5f, 0x0e, 0x1a, 0xb6, 0xdb, 0x34, 0x3c, 0xbb, 0x69,
+	0xba, 0x3e, 0x6e, 0x1e, 0x3c, 0xd7, 0xec, 0x61, 0x07, 0xfb, 0x46, 0x88, 0x2d, 0x66, 0xb1, 0x78,
+	0x51, 0xd2, 0x71, 0x70, 0x78, 0xd3, 0xf5, 0xf7, 0x6d, 0xa7, 0x97, 0xa5, 0x59, 0xef, 0xb9, 0x6e,
+	0xaf, 0x8f, 0x9b, 0xf4, 0xa9, 0x1b, 0xed, 0x36, 0x43, 0x7b, 0x80, 0x83, 0xd0, 0x18, 0x78, 0x5c,
+	0xe1, 0x85, 0x84, 0x6a, 0x60, 0x98, 0x7b, 0xb6, 0x83, 0xfd, 0xa3, 0x26, 0x6d, 0xaf, 0x67, 0x37,
+	0x7d, 0x1c, 0xb8, 0x91, 0x6f, 0xe2, 0x31, 0xda, 0x67, 0x7a, 0x76, 0xb8, 0x17, 0x75, 0x1b, 0xa6,
+	0x3b, 0x68, 0xf6, 0xdc, 0x9e, 0x9b, 0xf0, 0x93, 0x27, 0xfa, 0x40, 0xff, 0x63, 0xea, 0xfa, 0x2f,
+	0x10, 0xcc, 0xad, 0x91, 0xd7, 0xdb, 0xc6, 0x1f, 0x45, 0xd8, 0x31, 0x31, 0x7a, 0x12, 0xa6, 0x3f,
+	0x8a, 0x70, 0x84, 0x6b, 0xda, 0x05, 0xed, 0x62, 0xb9, 0x75, 0x6a, 0x34, 0xac, 0x57, 0x28, 0xf0,
+	0xb4, 0x3b, 0xb0, 0x43, 0x3c, 0xf0, 0xc2, 0xa3, 0x36, 0xd3, 0x40, 0xaf, 0xc2, 0xec, 0x0d, 0xb7,
+	0xdb, 0x09, 0x70, 0xd8, 0x71, 0x8c, 0x01, 0xae, 0xe5, 0xa8, 0x45, 0x6d, 0x34, 0xac, 0x2f, 0xdc,
+	0x70, 0xbb, 0xdb, 0x38, 0xdc, 0x34, 0x06, 0xb2, 0x19, 0x24, 0x28, 0x7a, 0x06, 0x8a, 0x51, 0x80,
+	0xfd, 0x8e, 0x6d, 0xd5, 0xf2, 0xd4, 0x6c, 0x61, 0x34, 0xac, 0x57, 0x09, 0x74, 0xc5, 0x92, 0x4c,
+	0x0a, 0x0c, 0x41, 0x4f, 0x43, 0xa1, 0xe7, 0xbb, 0x91, 0x17, 0xd4, 0xa6, 0x2e, 0xe4, 0x85, 0x36,
+	0x43, 0x64, 0x6d, 0x86, 0xa0, 0x6b, 0x50, 0x60, 0x31, 0xab, 0x4d, 0x5f, 0xc8, 0x5f, 0x9c, 0x59,
+	0x7e, 0xac, 0x21, 0x07, 0xb2, 0xa1, 0xbc, 0x30, 0x7b, 0x62, 0x84, 0x4c, 0x2e, 0x13, 0xf2, 0xd0,
+	0xff, 0xbb, 0x02, 0xd3, 0x54, 0x0f, 0x5d, 0x83, 0xa2, 0xe9, 0x63, 0xd2, 0xe1, 0x35, 0x74, 0x41,
+	0xbb, 0x38, 0xb3, 0xbc, 0xd8, 0x60, 0x81, 0x6c, 0x88, 0x8e, 0x6e, 0x5c, 0x17, 0x81, 0x6c, 0x9d,
+	0x1d, 0x0d, 0xeb, 0x27, 0xb9, 0x7a, 0xc2, 0xfa, 0xe9, 0x9f, 0xeb, 0x5a, 0x5b, 0xb0, 0xa0, 0x2d,
+	0x28, 0x07, 0x51, 0x77, 0x60, 0x87, 0x57, 0xdd, 0x2e, 0xed, 0xf3, 0x99, 0xe5, 0x47, 0xd4, 0xe6,
+	0x6e, 0x0b, 0x71, 0xeb, 0x91, 0xd1, 0xb0, 0x7e, 0x2a, 0xd6, 0x4e, 0x18, 0x2f, 0x9f, 0x68, 0x27,
+	0x24, 0x68, 0x0f, 0x2a, 0x3e, 0xf6, 0x7c, 0xdb, 0xf5, 0xed, 0xd0, 0x0e, 0x30, 0xe1, 0xcd, 0x51,
+	0xde, 0xf3, 0x2a, 0x6f, 0x5b, 0x55, 0x6a, 0x9d, 0x1f, 0x0d, 0xeb, 0x67, 0x53, 0x96, 0x8a, 0x8f,
+	0x34, 0x2d, 0x0a, 0x01, 0xa5, 0xa0, 0x6d, 0x1c, 0xd2, 0x78, 0xce, 0x2c, 0x5f, 0xb8, 0xa5, 0xb3,
+	0x6d, 0x1c, 0xb6, 0x2e, 0x8c, 0x86, 0xf5, 0x73, 0xe3, 0xf6, 0x8a, 0xcb, 0x0c, 0x7e, 0xd4, 0x87,
+	0xaa, 0x8c, 0x5a, 0xe4, 0x05, 0xa7, 0xa8, 0xcf, 0xa5, 0xc9, 0x3e, 0x89, 0x56, 0x6b, 0x69, 0x34,
+	0xac, 0x2f, 0xa6, 0x6d, 0x15, 0x7f, 0x63, 0xcc, 0x24, 0x3e, 0xa6, 0xe1, 0x98, 0xb8, 0x4f, 0xdc,
+	0x4c, 0x67, 0xc5, 0xe7, 0x92, 0x10, 0xb3, 0xf8, 0xc4, 0xda, 0x6a, 0x7c, 0x62, 0x18, 0x7d, 0x00,
+	0xb3, 0xf1, 0x03, 0xe9, 0xaf, 0x02, 0x1f, 0x47, 0xd9, 0xa4, 0xa4, 0xa7, 0x16, 0x47, 0xc3, 0xfa,
+	0x19, 0xd9, 0x46, 0xa1, 0x56, 0xd8, 0x12, 0xf6, 0x3e, 0xeb, 0x99, 0xe2, 0x64, 0x76, 0xa6, 0x21,
+	0xb3, 0xf7, 0xc7, 0x7b, 0x44, 0x61, 0x23, 0xec, 0x64, 0x12, 0x47, 0xa6, 0x89, 0xb1, 0x85, 0xad,
+	0x5a, 0x29, 0x8b, 0xfd, 0xaa, 0xa4, 0xc1, 0xd8, 0x65, 0x1b, 0x95, 0x5d, 0x96, 0x90, 0xbe, 0xbe,
+	0xe1, 0x76, 0xd7, 0x7c, 0xdf, 0xf5, 0x83, 0x5a, 0x39, 0xab, 0xaf, 0xaf, 0x0a, 0x31, 0xeb, 0xeb,
+	0x58, 0x5b, 0xed, 0xeb, 0x18, 0xe6, 0xed, 0x6d, 0x47, 0xce, 0x3a, 0x36, 0x02, 0x6c, 0xd5, 0x60,
+	0x42, 0x7b, 0x63, 0x8d, 0xb8, 0xbd, 0x31, 0x32, 0xd6, 0xde, 0x58, 0x82, 0x2c, 0x98, 0x67, 0xcf,
+	0x2b, 0x41, 0x60, 0xf7, 0x1c, 0x6c, 0xd5, 0x66, 0x28, 0xff, 0xb9, 0x2c, 0x7e, 0xa1, 0xd3, 0x3a,
+	0x37, 0x1a, 0xd6, 0x6b, 0xaa, 0x9d, 0xe2, 0x23, 0xc5, 0x89, 0x3e, 0x84, 0x39, 0x86, 0xb4, 0x23,
+	0xc7, 0xb1, 0x9d, 0x5e, 0x6d, 0x96, 0x3a, 0x79, 0x34, 0xcb, 0x09, 0x57, 0x69, 0x3d, 0x3a, 0x1a,
+	0xd6, 0x1f, 0x51, 0xac, 0x14, 0x17, 0x2a, 0x21, 0xc9, 0x18, 0x0c, 0x48, 0x02, 0x3b, 0x97, 0x95,
+	0x31, 0xae, 0xaa, 0x4a, 0x2c, 0x63, 0xa4, 0x2c, 0xd5, 0x8c, 0x91, 0x12, 0x26, 0xf1, 0xe0, 0x41,
+	0x9e, 0x9f, 0x1c, 0x0f, 0x1e, 0x67, 0x29, 0x1e, 0x19, 0xa1, 0x56, 0xd8, 0xd0, 0xc7, 0x40, 0x16,
+	0x9e, 0xd5, 0xc8, 0xeb, 0xdb, 0xa6, 0x11, 0xe2, 0x55, 0x1c, 0x62, 0x93, 0x64, 0xea, 0x0a, 0xf5,
+	0xa2, 0x8f, 0x79, 0x19, 0xd3, 0x6c, 0xe9, 0xa3, 0x61, 0x7d, 0x29, 0x8b, 0x43, 0xf1, 0x9a, 0xe9,
+	0x05, 0x7d, 0x4f, 0x83, 0xd3, 0x41, 0x68, 0x38, 0x96, 0xd1, 0x77, 0x1d, 0x7c, 0xc5, 0xe9, 0xf9,
+	0x38, 0x08, 0xae, 0x38, 0xbb, 0x6e, 0xad, 0x4a, 0xfd, 0x3f, 0x9e, 0x4a, 0xeb, 0x59, 0xaa, 0xad,
+	0xc7, 0x47, 0xc3, 0x7a, 0x3d, 0x93, 0x45, 0x69, 0x41, 0xb6, 0x23, 0x74, 0x08, 0xa7, 0x44, 0x65,
+	0xb0, 0x13, 0xda, 0x7d, 0x3b, 0x30, 0x42, 0xdb, 0x75, 0x6a, 0x27, 0xa9, 0xff, 0xc7, 0xd2, 0xd9,
+	0x71, 0x4c, 0xb1, 0xf5, 0xd8, 0x68, 0x58, 0x3f, 0x9f, 0xc1, 0xa0, 0xf8, 0xce, 0x72, 0x91, 0x0c,
+	0xa1, 0x2d, 0x1f, 0x13, 0x45, 0x6c, 0xd5, 0x4e, 0x4d, 0x1e, 0x42, 0xb1, 0x92, 0x3c, 0x84, 0x62,
+	0x30, 0x6b, 0x08, 0xc5, 0x42, 0xe2, 0xc9, 0x33, 0xfc, 0xd0, 0x26, 0x6e, 0x37, 0x0c, 0x7f, 0x1f,
+	0xfb, 0xb5, 0x85, 0x2c, 0x4f, 0x5b, 0xaa, 0x12, 0xf3, 0x94, 0xb2, 0x54, 0x3d, 0xa5, 0x84, 0xad,
+	0x22, 0x4c, 0x53, 0x2e, 0x7d, 0x54, 0x80, 0x53, 0x19, 0xdd, 0x85, 0x5e, 0x87, 0x82, 0x1f, 0x39,
+	0xa4, 0x86, 0x61, 0x0b, 0x37, 0x52, 0x5b, 0xb0, 0x13, 0xd9, 0x16, 0x2b, 0xa0, 0xfc, 0xc8, 0x51,
+	0xca, 0x9a, 0x69, 0x0a, 0x10, 0x7b, 0x52, 0x40, 0xd9, 0x16, 0x5f, 0xa0, 0x27, 0xda, 0xdf, 0x70,
+	0xbb, 0xaa, 0x3d, 0x05, 0x10, 0x86, 0x39, 0x11, 0x8b, 0x8e, 0x4d, 0x06, 0x1a, 0x5b, 0x7a, 0x9f,
+	0x50, 0x69, 0xde, 0x8e, 0xba, 0xd8, 0x77, 0x70, 0x88, 0x03, 0xf1, 0x0e, 0x74, 0xa4, 0xd1, 0x89,
+	0xe5, 0x4b, 0x88, 0xc4, 0x3f, 0x2b, 0xe3, 0xe8, 0xa7, 0x1a, 0xd4, 0x06, 0xc6, 0x61, 0x47, 0x80,
+	0x41, 0x67, 0xd7, 0xf5, 0x3b, 0x1e, 0xf6, 0x6d, 0xd7, 0xa2, 0xf5, 0xd8, 0xcc, 0xf2, 0xb7, 0x6e,
+	0x3b, 0xb6, 0x1a, 0x1b, 0xc6, 0xa1, 0x80, 0x83, 0x37, 0x5d, 0x7f, 0x8b, 0x9a, 0xaf, 0x39, 0xa1,
+	0x7f, 0xd4, 0x3a, 0xff, 0xc5, 0xb0, 0x7e, 0x62, 0x34, 0xac, 0x9f, 0x1e, 0x64, 0xe9, 0xb4, 0xb3,
+	0x61, 0xf4, 0x13, 0x0d, 0xce, 0x84, 0x6e, 0x68, 0xf4, 0x3b, 0x66, 0x34, 0x88, 0xfa, 0x46, 0x68,
+	0x1f, 0xe0, 0x4e, 0x14, 0x18, 0x3d, 0xcc, 0xcb, 0xbe, 0xd7, 0x6e, 0xdf, 0xa8, 0xeb, 0xc4, 0xfe,
+	0x52, 0x6c, 0xbe, 0x43, 0xac, 0x59, 0x9b, 0xce, 0xf1, 0x36, 0x2d, 0x84, 0x19, 0x2a, 0xed, 0x4c,
+	0x74, 0xf1, 0xe7, 0x1a, 0x2c, 0x4e, 0x7e, 0x4d, 0xf4, 0x38, 0xe4, 0xf7, 0xf1, 0x11, 0x2f, 0xac,
+	0x4f, 0x8e, 0x86, 0xf5, 0xb9, 0x7d, 0x7c, 0x24, 0xf5, 0x3a, 0x91, 0xa2, 0xef, 0xc0, 0xf4, 0x81,
+	0xd1, 0x8f, 0x30, 0x1f, 0x12, 0x8d, 0x06, 0xdb, 0x06, 0x34, 0xe4, 0x6d, 0x40, 0xc3, 0xdb, 0xef,
+	0x11, 0xa0, 0x21, 0x22, 0xd2, 0x78, 0x27, 0x32, 0x9c, 0xd0, 0x0e, 0x8f, 0xd8, 0x70, 0xa1, 0x04,
+	0xf2, 0x70, 0xa1, 0xc0, 0xab, 0xb9, 0x97, 0xb5, 0xc5, 0xcf, 0x34, 0x38, 0x3b, 0xf1, 0xa5, 0xbf,
+	0x09, 0x2d, 0xd4, 0x3b, 0x30, 0x45, 0x06, 0x3e, 0x29, 0xf9, 0xf7, 0xec, 0xde, 0xde, 0x4b, 0x2f,
+	0xd0, 0xe6, 0x14, 0x58, 0x85, 0xce, 0x10, 0xb9, 0x42, 0x67, 0x08, 0xd9, 0xb6, 0xf4, 0xdd, 0x9b,
+	0x2f, 0xbd, 0x40, 0x1b, 0x55, 0x60, 0x4e, 0x28, 0x20, 0x3b, 0xa1, 0x80, 0xfe, 0xf7, 0x69, 0x28,
+	0xc7, 0x35, 0xb5, 0x34, 0x07, 0xb5, 0x3b, 0x9a, 0x83, 0x97, 0xa1, 0x6a, 0x61, 0x8b, 0x2f, 0x06,
+	0xb6, 0xeb, 0x88, 0xd9, 0x5c, 0x66, 0x09, 0x47, 0x91, 0x29, 0xf6, 0x95, 0x94, 0x08, 0x2d, 0x43,
+	0x89, 0xd7, 0x9e, 0x47, 0x74, 0x22, 0xcf, 0xb5, 0xce, 0x8c, 0x86, 0x75, 0x24, 0x30, 0xc9, 0x34,
+	0xd6, 0x43, 0x6d, 0x00, 0xb7, 0x7b, 0x03, 0x9b, 0xe1, 0x06, 0x0e, 0x0d, 0x5e, 0x05, 0xd7, 0xd4,
+	0x37, 0xb8, 0x16, 0xcb, 0xd9, 0xd6, 0x2c, 0xd1, 0x97, 0xb7, 0x66, 0x09, 0x8a, 0x3e, 0x00, 0x18,
+	0x18, 0xb6, 0xc3, 0xec, 0x78, 0xc9, 0xab, 0x4f, 0x4a, 0x29, 0x1b, 0xb1, 0x26, 0x63, 0x4f, 0x2c,
+	0x65, 0xf6, 0x04, 0x25, 0x1b, 0x28, 0xe6, 0x2b, 0xa8, 0x15, 0xe8, 0x2c, 0x5d, 0x9a, 0x44, 0xcd,
+	0x69, 0x4f, 0x93, 0x4d, 0x14, 0x37, 0x91, 0x38, 0x05, 0x0b, 0xe9, 0xb6, 0xbe, 0xbd, 0x8b, 0xc9,
+	0xf6, 0x99, 0x16, 0xbb, 0xbc, 0xdb, 0x04, 0x26, 0x77, 0x9b, 0xc0, 0xd0, 0xcb, 0x00, 0x46, 0xb8,
+	0xe1, 0x06, 0xe1, 0x35, 0xc7, 0xc4, 0xb4, 0x88, 0x2d, 0xb1, 0xe6, 0x27, 0xa8, 0xdc, 0xfc, 0x04,
+	0x45, 0xaf, 0xc1, 0x8c, 0xc7, 0x96, 0x22, 0xbb, 0xdb, 0xc7, 0xb4, 0x48, 0x2d, 0xd1, 0x7d, 0xde,
+	0x69, 0x09, 0x96, 0x6c, 0x65, 0x6d, 0xf4, 0x16, 0x54, 0x4c, 0xd7, 0x31, 0x23, 0xdf, 0xc7, 0x8e,
+	0x79, 0xb4, 0x6d, 0xec, 0x62, 0x5a, 0x90, 0x96, 0xd8, 0x50, 0x49, 0x89, 0xe4, 0xa1, 0x92, 0x12,
+	0xa1, 0x17, 0xa1, 0x1c, 0x98, 0x7b, 0xd8, 0x8a, 0xfa, 0xd8, 0xa7, 0x35, 0x67, 0x99, 0xef, 0x0d,
+	0x05, 0x28, 0x19, 0x27, 0x9a, 0xfa, 0xef, 0x35, 0x58, 0xc8, 0x0a, 0x5d, 0x6a, 0x18, 0x69, 0xf7,
+	0x64, 0x18, 0xbd, 0x0b, 0x25, 0xcf, 0xb5, 0x3a, 0x81, 0x87, 0x4d, 0x9e, 0x29, 0x52, 0x83, 0x68,
+	0xcb, 0xb5, 0xb6, 0x3d, 0x6c, 0x7e, 0xdb, 0x0e, 0xf7, 0x56, 0x0e, 0x5c, 0xdb, 0x5a, 0xb7, 0x03,
+	0x1e, 0x6d, 0x8f, 0x49, 0x94, 0xd5, 0xb9, 0xc8, 0xc1, 0x56, 0x09, 0x0a, 0xcc, 0x8b, 0xfe, 0x87,
+	0x3c, 0x54, 0xd3, 0xc3, 0xe5, 0x7f, 0xe9, 0x55, 0xd0, 0x7b, 0x50, 0xb4, 0x59, 0xf5, 0xc6, 0x57,
+	0xee, 0xff, 0x93, 0x72, 0x69, 0x23, 0x39, 0x3f, 0x6a, 0x1c, 0x3c, 0xd7, 0xe0, 0x65, 0x1e, 0xed,
+	0x02, 0xca, 0xcc, 0x2d, 0x55, 0x66, 0x0e, 0xa2, 0x36, 0x14, 0x03, 0xec, 0x1f, 0xd8, 0x26, 0xe6,
+	0x49, 0xa1, 0x2e, 0x33, 0x9b, 0xae, 0x8f, 0x09, 0xe7, 0x36, 0x53, 0x49, 0x38, 0xb9, 0x8d, 0xca,
+	0xc9, 0x41, 0xf4, 0x2e, 0x94, 0x4d, 0xd7, 0xd9, 0xb5, 0x7b, 0x1b, 0x86, 0xc7, 0xd3, 0xc2, 0xf9,
+	0x2c, 0xd6, 0x4b, 0x42, 0x89, 0xef, 0x87, 0xc5, 0x63, 0x6a, 0x3f, 0x1c, 0x6b, 0x25, 0x01, 0xfd,
+	0xdb, 0x14, 0x40, 0x12, 0x1c, 0xf4, 0x0a, 0xcc, 0xe0, 0x43, 0x6c, 0x46, 0xa1, 0xeb, 0x8b, 0xfc,
+	0xcc, 0x8f, 0x97, 0x04, 0xac, 0x24, 0x54, 0x48, 0x50, 0x32, 0x41, 0x1c, 0x63, 0x80, 0x03, 0xcf,
+	0x30, 0xc5, 0xb9, 0x14, 0x6d, 0x4c, 0x0c, 0xca, 0x13, 0x24, 0x06, 0xd1, 0xff, 0xc3, 0x14, 0x3d,
+	0xc9, 0x62, 0x47, 0x52, 0x68, 0x34, 0xac, 0xcf, 0x3b, 0xea, 0x19, 0x16, 0x95, 0xa3, 0x37, 0x60,
+	0x6e, 0x3f, 0x1e, 0x78, 0xa4, 0x6d, 0x53, 0xd4, 0x80, 0x96, 0x54, 0x89, 0x40, 0x69, 0xdd, 0xac,
+	0x8c, 0xa3, 0x5d, 0x98, 0x31, 0x1c, 0xc7, 0x0d, 0x69, 0xee, 0x17, 0xc7, 0x54, 0x4f, 0x4e, 0x1a,
+	0xa6, 0x8d, 0x95, 0x44, 0x97, 0x55, 0x27, 0x34, 0xe3, 0x48, 0x0c, 0x72, 0xc6, 0x91, 0x60, 0xd4,
+	0x86, 0x42, 0xdf, 0xe8, 0xe2, 0xbe, 0x48, 0xb6, 0x4f, 0x4c, 0x74, 0xb1, 0x4e, 0xd5, 0x18, 0x3b,
+	0x5d, 0x6a, 0x99, 0x9d, 0xbc, 0xd4, 0x32, 0x64, 0x71, 0x17, 0xaa, 0xe9, 0xf6, 0x1c, 0xaf, 0x70,
+	0x78, 0x52, 0x2e, 0x1c, 0xca, 0xb7, 0x2d, 0x55, 0x0c, 0x98, 0x91, 0x1a, 0x75, 0x3f, 0x5c, 0xe8,
+	0xbf, 0xd4, 0x60, 0x21, 0x6b, 0xee, 0xa2, 0x0d, 0x69, 0xc6, 0x6b, 0x7c, 0xbb, 0x9d, 0x31, 0xd4,
+	0xb9, 0xed, 0x84, 0xa9, 0x9e, 0x4c, 0xf4, 0x16, 0xcc, 0x3b, 0xae, 0x85, 0x3b, 0x06, 0x71, 0xd0,
+	0xb7, 0x83, 0xb0, 0x96, 0xa3, 0xc7, 0x98, 0x74, 0x9b, 0x4e, 0x24, 0x2b, 0x42, 0x20, 0x59, 0xcf,
+	0x29, 0x02, 0xfd, 0x87, 0x1a, 0x54, 0x52, 0xa7, 0x68, 0x77, 0x5d, 0xbc, 0xc8, 0x25, 0x47, 0xee,
+	0x78, 0x25, 0x87, 0x7e, 0x19, 0xd0, 0xf8, 0x61, 0x9e, 0xc2, 0xa4, 0x1d, 0x93, 0xe9, 0x13, 0x0d,
+	0xaa, 0xe9, 0x33, 0xba, 0x07, 0xf2, 0x4a, 0x6f, 0x43, 0x39, 0x3e, 0x6f, 0xbb, 0xdb, 0x06, 0xe8,
+	0xd7, 0x61, 0x96, 0xf5, 0xc9, 0x9b, 0x76, 0x3f, 0xc4, 0x3e, 0x5a, 0x85, 0x42, 0x10, 0x1a, 0x21,
+	0x0e, 0x6a, 0xda, 0x85, 0xfc, 0xc5, 0xf9, 0xe5, 0x33, 0xe3, 0x87, 0x65, 0x44, 0xcc, 0x26, 0x1d,
+	0xd3, 0x94, 0x27, 0x1d, 0x43, 0x08, 0xab, 0x7c, 0x24, 0x78, 0x8f, 0x58, 0x37, 0x05, 0x6b, 0xff,
+	0x9e, 0x74, 0xbe, 0xfe, 0x6b, 0x8d, 0xbd, 0x7c, 0x7c, 0xde, 0x73, 0xb7, 0xd1, 0xec, 0x25, 0x3b,
+	0x5c, 0xb2, 0x15, 0x0d, 0xe8, 0xbc, 0x39, 0xee, 0x0e, 0x97, 0xce, 0x2e, 0xc5, 0x5c, 0x9e, 0x5d,
+	0x8a, 0x40, 0xff, 0x2d, 0x6b, 0x79, 0x72, 0xb6, 0xf7, 0xa0, 0xf7, 0xf6, 0xa9, 0xc5, 0x2f, 0x7f,
+	0xfc, 0xc5, 0x4f, 0xff, 0xa7, 0x06, 0xf3, 0xea, 0x99, 0xe3, 0x03, 0x7f, 0x9b, 0xb1, 0x38, 0xe6,
+	0xef, 0x53, 0x1c, 0xff, 0xa1, 0xc1, 0x9c, 0x72, 0x14, 0xfa, 0xf0, 0xbc, 0xfa, 0xcf, 0x72, 0x70,
+	0x26, 0x9b, 0xe6, 0xbe, 0x14, 0xc5, 0x97, 0x81, 0x2c, 0x6f, 0xf4, 0x7c, 0x93, 0x75, 0xcc, 0xe9,
+	0xb1, 0x9a, 0x98, 0xbe, 0x82, 0x58, 0x1b, 0xc7, 0xce, 0x30, 0x85, 0x39, 0x7a, 0x0f, 0x66, 0x6c,
+	0xe9, 0xb4, 0x94, 0x95, 0xc2, 0x67, 0x55, 0x36, 0xf9, 0x8c, 0x94, 0x16, 0x3f, 0xf6, 0x84, 0x93,
+	0x51, 0x99, 0xaa, 0x55, 0x80, 0x29, 0xdb, 0xd9, 0x75, 0xf5, 0x03, 0x28, 0xf2, 0xe6, 0xa0, 0xe7,
+	0xa1, 0x4c, 0x97, 0x62, 0x5a, 0xe7, 0xb1, 0x62, 0x82, 0x2e, 0x10, 0x04, 0x4c, 0xdd, 0x57, 0x96,
+	0x04, 0x86, 0x5e, 0x02, 0x20, 0xe5, 0x80, 0x13, 0x0d, 0xba, 0xd8, 0xa7, 0xaf, 0x3b, 0xcd, 0xea,
+	0x49, 0xcf, 0xb5, 0x36, 0x29, 0x28, 0xd7, 0x93, 0x31, 0xa8, 0xff, 0x2a, 0x07, 0x33, 0xf2, 0xf9,
+	0xec, 0x1d, 0x39, 0xff, 0x18, 0x44, 0xad, 0xdf, 0x31, 0x2c, 0x8b, 0xfc, 0xc5, 0x22, 0x0f, 0x36,
+	0x27, 0x76, 0x92, 0xf8, 0x7f, 0x45, 0x58, 0xb0, 0xca, 0x8e, 0xde, 0x80, 0xd9, 0x29, 0x91, 0xe4,
+	0xb5, 0x9a, 0x96, 0x2d, 0xee, 0xc3, 0xe9, 0x4c, 0x2a, 0xb9, 0x1e, 0x9b, 0xbe, 0x57, 0xf5, 0xd8,
+	0x6f, 0xa6, 0xe1, 0x74, 0xe6, 0xb9, 0xf8, 0x03, 0x9f, 0xc5, 0xea, 0x0c, 0xca, 0xdf, 0x93, 0x19,
+	0xf4, 0x89, 0x96, 0x15, 0x59, 0x76, 0xa0, 0xfa, 0xca, 0x31, 0x2e, 0x0b, 0xee, 0x55, 0x8c, 0xd5,
+	0x61, 0x39, 0x7d, 0x47, 0x73, 0xa2, 0x70, 0xdc, 0x39, 0x81, 0x9e, 0x65, 0xa5, 0x35, 0xf5, 0x55,
+	0xa4, 0xbe, 0x44, 0x86, 0x48, 0xb9, 0x2a, 0x72, 0x88, 0xec, 0xb6, 0x84, 0x05, 0xdb, 0xd0, 0x95,
+	0x92, 0xdd, 0x16, 0xd7, 0x49, 0xef, 0xe9, 0x66, 0x65, 0xfc, 0xbf, 0x3b, 0x86, 0xff, 0xa5, 0x41,
+	0x25, 0x75, 0x51, 0xf6, 0xf0, 0xac, 0x41, 0x3f, 0xd6, 0xa0, 0x1c, 0xdf, 0xd1, 0xde, 0x75, 0xf5,
+	0xb7, 0x02, 0x05, 0xcc, 0xee, 0x09, 0x59, 0xba, 0x3b, 0x95, 0xfa, 0x8e, 0x83, 0xc8, 0xf8, 0x97,
+	0x1b, 0xa9, 0xab, 0xc1, 0x36, 0x37, 0xd4, 0xff, 0x18, 0xd7, 0x75, 0x49, 0x9b, 0x1e, 0x68, 0x28,
+	0x92, 0x77, 0xca, 0xdf, 0xe9, 0x3b, 0xfd, 0xae, 0x08, 0xd3, 0x54, 0x8f, 0x6c, 0x76, 0x42, 0xec,
+	0x0f, 0x6c, 0xc7, 0xe8, 0xd3, 0xd7, 0x29, 0xb1, 0x79, 0x2b, 0x30, 0x79, 0xde, 0x0a, 0x0c, 0xed,
+	0x41, 0x25, 0x39, 0x8a, 0xa0, 0x34, 0xd9, 0x9f, 0x87, 0xbc, 0xad, 0x2a, 0xb1, 0x33, 0xca, 0x94,
+	0xa5, 0x7a, 0x7f, 0x96, 0x12, 0x22, 0x0b, 0xe6, 0x4d, 0xd7, 0x09, 0x0d, 0xdb, 0xc1, 0x3e, 0x73,
+	0x94, 0xcf, 0xba, 0x1e, 0xbf, 0xa4, 0xe8, 0xb0, 0xeb, 0x71, 0xd5, 0x4e, 0xbd, 0x1e, 0x57, 0x65,
+	0xe8, 0x43, 0x98, 0x13, 0xb5, 0x2f, 0x73, 0x32, 0x95, 0x75, 0x3d, 0xbe, 0x26, 0xab, 0xb0, 0x21,
+	0xad, 0x58, 0xa9, 0xd7, 0xe3, 0x8a, 0x08, 0xf5, 0xa1, 0xea, 0xb9, 0xd6, 0x8e, 0xc3, 0x0f, 0x52,
+	0x8d, 0x6e, 0x1f, 0xf3, 0xf3, 0xaf, 0xa5, 0xb1, 0x92, 0x47, 0xd1, 0x62, 0xa9, 0x38, 0x6d, 0xab,
+	0x7e, 0x70, 0x92, 0x96, 0xa2, 0x0f, 0x60, 0xb6, 0x4f, 0xb6, 0x20, 0x6b, 0x87, 0x9e, 0xed, 0x63,
+	0x2b, 0xfb, 0xf3, 0x90, 0x75, 0x49, 0x83, 0x25, 0x42, 0xd9, 0x46, 0xbd, 0x22, 0x97, 0x25, 0x24,
+	0xfa, 0x03, 0xe3, 0xb0, 0x1d, 0x39, 0xc1, 0xda, 0x21, 0xbf, 0xea, 0x2f, 0x66, 0x45, 0x7f, 0x43,
+	0x55, 0x62, 0xd1, 0x4f, 0x59, 0xaa, 0xd1, 0x4f, 0x09, 0xd1, 0x3a, 0xcd, 0xf3, 0x2c, 0x24, 0xec,
+	0x33, 0x91, 0x33, 0x63, 0xbd, 0xc5, 0xa2, 0xc1, 0x36, 0xe8, 0xfc, 0x49, 0x21, 0x8d, 0x19, 0x78,
+	0x0c, 0xe8, 0x6b, 0xb7, 0x71, 0x18, 0xf9, 0x0e, 0xb6, 0xf8, 0x17, 0x22, 0xe3, 0x31, 0x50, 0xb4,
+	0xe2, 0x18, 0x28, 0xe8, 0x58, 0x0c, 0x14, 0x29, 0x19, 0x53, 0x9e, 0x6b, 0x5d, 0x67, 0x53, 0x26,
+	0x8c, 0xbf, 0x1b, 0x79, 0x74, 0xcc, 0x55, 0xa2, 0xc2, 0xc6, 0x94, 0x62, 0xa5, 0x8e, 0x29, 0x45,
+	0xd4, 0x2a, 0x41, 0xc1, 0xc7, 0x46, 0xe0, 0x3a, 0xfa, 0x67, 0x1a, 0x54, 0x52, 0x73, 0x0d, 0xbd,
+	0x0e, 0xf1, 0x0d, 0xec, 0xf5, 0x23, 0x4f, 0x94, 0x8a, 0xca, 0x8d, 0x2d, 0xc1, 0xb3, 0x6e, 0x6c,
+	0x09, 0x8e, 0xd6, 0x01, 0xe2, 0xbc, 0x7c, 0xab, 0x44, 0x45, 0xeb, 0x94, 0x44, 0x53, 0xae, 0x53,
+	0x12, 0x54, 0xff, 0x32, 0x0f, 0x25, 0x11, 0xac, 0xfb, 0xb2, 0x95, 0x68, 0x42, 0x71, 0x80, 0x03,
+	0x7a, 0x73, 0x9b, 0x4b, 0x2a, 0x02, 0x0e, 0xc9, 0x15, 0x01, 0x87, 0xd4, 0x82, 0x25, 0x7f, 0x47,
+	0x05, 0xcb, 0xd4, 0xb1, 0x0b, 0x16, 0x4c, 0x6f, 0x6d, 0xa4, 0x94, 0x23, 0xce, 0x6b, 0x6f, 0x9d,
+	0xc7, 0xc4, 0x9d, 0x8e, 0x6c, 0x98, 0xba, 0xd3, 0x91, 0x45, 0x68, 0x1f, 0x4e, 0x4a, 0x67, 0xca,
+	0x6c, 0x70, 0xd0, 0xc9, 0x3f, 0x3f, 0xf9, 0x8a, 0xac, 0x4d, 0xb5, 0xd8, 0x10, 0xdf, 0x4f, 0xa1,
+	0x72, 0xc5, 0x97, 0x96, 0xe9, 0x7f, 0xcd, 0xc1, 0xbc, 0xda, 0xde, 0xfb, 0x12, 0xd8, 0xe7, 0xa1,
+	0x8c, 0x0f, 0xed, 0xb0, 0x63, 0xba, 0x16, 0xe6, 0xdb, 0x26, 0x1a, 0x27, 0x02, 0x5e, 0x72, 0x2d,
+	0x25, 0x4e, 0x02, 0x93, 0x47, 0x43, 0xfe, 0x58, 0xa3, 0xe1, 0x69, 0x31, 0x97, 0xf8, 0x31, 0x3c,
+	0x5d, 0x3d, 0xfd, 0x74, 0x27, 0x70, 0x9d, 0xec, 0x7e, 0x2e, 0xdf, 0xa7, 0x7e, 0xfe, 0x34, 0x07,
+	0xd5, 0x74, 0x46, 0xfa, 0x66, 0x4c, 0x21, 0x75, 0x36, 0xe4, 0x8f, 0x3d, 0x1b, 0xde, 0x80, 0x39,
+	0x52, 0x3f, 0x19, 0x61, 0xc8, 0x3f, 0xf3, 0x99, 0xa2, 0x75, 0x07, 0xcb, 0x4d, 0x91, 0xb3, 0x22,
+	0x70, 0x25, 0x37, 0x49, 0xb8, 0xfe, 0xfd, 0x1c, 0xcc, 0x29, 0x99, 0xf3, 0xe1, 0x4b, 0x29, 0x7a,
+	0x05, 0xe6, 0x94, 0x82, 0x44, 0xff, 0x01, 0x1b, 0x27, 0x6a, 0x25, 0xf0, 0xf0, 0xf5, 0xcb, 0x3c,
+	0xcc, 0xca, 0x95, 0x8d, 0x7e, 0x12, 0x2a, 0xa9, 0x42, 0x44, 0xff, 0x5c, 0x83, 0x85, 0xac, 0x4f,
+	0xf7, 0xd0, 0x65, 0x00, 0x07, 0xdf, 0xec, 0xdc, 0x76, 0xc3, 0xc1, 0x5a, 0x8f, 0x6f, 0x5e, 0x4d,
+	0xd5, 0xe7, 0x25, 0x81, 0x11, 0x26, 0xb7, 0x6f, 0x75, 0x6e, 0x5b, 0xe6, 0x53, 0x26, 0xb7, 0x6f,
+	0x8d, 0x31, 0x09, 0x4c, 0xff, 0x51, 0x5e, 0xec, 0x05, 0x93, 0xef, 0xd7, 0xde, 0x87, 0xaa, 0x27,
+	0x1e, 0x6e, 0xdf, 0x5a, 0x5a, 0x0d, 0xc7, 0xfa, 0x69, 0x4f, 0xf3, 0xaa, 0x44, 0xe5, 0xe6, 0xdb,
+	0x9c, 0xdc, 0x31, 0xb9, 0xdb, 0xa9, 0xfd, 0xce, 0xbc, 0x2a, 0x41, 0xdf, 0x85, 0x93, 0xe2, 0x5b,
+	0x86, 0x03, 0x2c, 0x1a, 0x9e, 0x9f, 0x48, 0xce, 0x3e, 0xb7, 0x8b, 0x0d, 0xd2, 0x2d, 0xaf, 0xa4,
+	0x44, 0x29, 0x7a, 0xde, 0xf6, 0xa9, 0xe3, 0xd2, 0xa7, 0x1b, 0x5f, 0x49, 0x89, 0xc8, 0xc6, 0xb4,
+	0x92, 0xfa, 0x22, 0x10, 0xad, 0x42, 0x89, 0xfe, 0x60, 0xe0, 0xd6, 0x11, 0xa0, 0x13, 0x84, 0xea,
+	0x29, 0x1e, 0x8a, 0x1c, 0x42, 0x2f, 0x42, 0x39, 0xfe, 0x70, 0x90, 0xdf, 0x38, 0xb1, 0xa1, 0x2e,
+	0x40, 0x65, 0xa8, 0x0b, 0xf0, 0xa9, 0x67, 0xa1, 0x24, 0x2e, 0x69, 0x10, 0x40, 0xe1, 0x9d, 0x9d,
+	0xb5, 0x9d, 0xb5, 0xd5, 0xea, 0x09, 0x34, 0x03, 0xc5, 0xad, 0xb5, 0xcd, 0xd5, 0x2b, 0x9b, 0x6f,
+	0x55, 0x35, 0xf2, 0xd0, 0xde, 0xd9, 0xdc, 0x24, 0x0f, 0xb9, 0xa7, 0xd6, 0xe5, 0xaf, 0x1d, 0xd8,
+	0xfa, 0x82, 0x66, 0xa1, 0xb4, 0xe2, 0x79, 0x34, 0x87, 0x30, 0xdb, 0xb5, 0x03, 0x9b, 0xcc, 0x86,
+	0xaa, 0x86, 0x8a, 0x90, 0xbf, 0x76, 0x6d, 0xa3, 0x9a, 0x43, 0x0b, 0x50, 0x5d, 0xc5, 0x86, 0xd5,
+	0xb7, 0x1d, 0x2c, 0x66, 0x51, 0x35, 0xdf, 0xba, 0xf1, 0xc5, 0x57, 0x4b, 0xda, 0x97, 0x5f, 0x2d,
+	0x69, 0x7f, 0xf9, 0x6a, 0x49, 0xfb, 0xf4, 0xeb, 0xa5, 0x13, 0x5f, 0x7e, 0xbd, 0x74, 0xe2, 0x4f,
+	0x5f, 0x2f, 0x9d, 0x78, 0xff, 0x59, 0xe9, 0x27, 0x24, 0xac, 0x3b, 0x3c, 0xdf, 0x25, 0x09, 0x84,
+	0x3f, 0x35, 0xd3, 0x3f, 0x9a, 0xf9, 0x3c, 0x77, 0x7e, 0x85, 0x3e, 0x6e, 0x31, 0xbd, 0xc6, 0x15,
+	0xb7, 0xc1, 0x00, 0xfa, 0x9b, 0x89, 0xa0, 0x5b, 0xa0, 0xbf, 0x8d, 0x78, 0xfe, 0x3f, 0x01, 0x00,
+	0x00, 0xff, 0xff, 0x44, 0xff, 0x4b, 0x40, 0x6f, 0x33, 0x00, 0x00,
 }
 
 func (m *EventSequence) Marshal() (dAtA []byte, err error) {
@@ -4539,6 +4629,47 @@ func (m *CancelJob) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	return len(dAtA) - i, nil
 }
 
+func (m *JobSetFilter) Marshal() (dAtA []byte, err error) {
+	size := m.Size()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalToSizedBuffer(dAtA[:size])
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *JobSetFilter) MarshalTo(dAtA []byte) (int, error) {
+	size := m.Size()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *JobSetFilter) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	if len(m.States) > 0 {
+		dAtA41 := make([]byte, len(m.States)*10)
+		var j40 int
+		for _, num := range m.States {
+			for num >= 1<<7 {
+				dAtA41[j40] = uint8(uint64(num)&0x7f | 0x80)
+				num >>= 7
+				j40++
+			}
+			dAtA41[j40] = uint8(num)
+			j40++
+		}
+		i -= j40
+		copy(dAtA[i:], dAtA41[:j40])
+		i = encodeVarintEvents(dAtA, i, uint64(j40))
+		i--
+		dAtA[i] = 0xa
+	}
+	return len(dAtA) - i, nil
+}
+
 func (m *CancelJobSet) Marshal() (dAtA []byte, err error) {
 	size := m.Size()
 	dAtA = make([]byte, size)
@@ -4559,6 +4690,24 @@ func (m *CancelJobSet) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	_ = i
 	var l int
 	_ = l
+	if len(m.States) > 0 {
+		dAtA43 := make([]byte, len(m.States)*10)
+		var j42 int
+		for _, num := range m.States {
+			for num >= 1<<7 {
+				dAtA43[j42] = uint8(uint64(num)&0x7f | 0x80)
+				num >>= 7
+				j42++
+			}
+			dAtA43[j42] = uint8(num)
+			j42++
+		}
+		i -= j42
+		copy(dAtA[i:], dAtA43[:j42])
+		i = encodeVarintEvents(dAtA, i, uint64(j42))
+		i--
+		dAtA[i] = 0xa
+	}
 	return len(dAtA) - i, nil
 }
 
@@ -6661,12 +6810,35 @@ func (m *CancelJob) Size() (n int) {
 	return n
 }
 
+func (m *JobSetFilter) Size() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	if len(m.States) > 0 {
+		l = 0
+		for _, e := range m.States {
+			l += sovEvents(uint64(e))
+		}
+		n += 1 + sovEvents(uint64(l)) + l
+	}
+	return n
+}
+
 func (m *CancelJobSet) Size() (n int) {
 	if m == nil {
 		return 0
 	}
 	var l int
 	_ = l
+	if len(m.States) > 0 {
+		l = 0
+		for _, e := range m.States {
+			l += sovEvents(uint64(e))
+		}
+		n += 1 + sovEvents(uint64(l)) + l
+	}
 	return n
 }
 
@@ -10382,6 +10554,125 @@ func (m *CancelJob) Unmarshal(dAtA []byte) error {
 	}
 	return nil
 }
+func (m *JobSetFilter) Unmarshal(dAtA []byte) error {
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return ErrIntOverflowEvents
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= uint64(b&0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: JobSetFilter: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: JobSetFilter: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType == 0 {
+				var v JobState
+				for shift := uint(0); ; shift += 7 {
+					if shift >= 64 {
+						return ErrIntOverflowEvents
+					}
+					if iNdEx >= l {
+						return io.ErrUnexpectedEOF
+					}
+					b := dAtA[iNdEx]
+					iNdEx++
+					v |= JobState(b&0x7F) << shift
+					if b < 0x80 {
+						break
+					}
+				}
+				m.States = append(m.States, v)
+			} else if wireType == 2 {
+				var packedLen int
+				for shift := uint(0); ; shift += 7 {
+					if shift >= 64 {
+						return ErrIntOverflowEvents
+					}
+					if iNdEx >= l {
+						return io.ErrUnexpectedEOF
+					}
+					b := dAtA[iNdEx]
+					iNdEx++
+					packedLen |= int(b&0x7F) << shift
+					if b < 0x80 {
+						break
+					}
+				}
+				if packedLen < 0 {
+					return ErrInvalidLengthEvents
+				}
+				postIndex := iNdEx + packedLen
+				if postIndex < 0 {
+					return ErrInvalidLengthEvents
+				}
+				if postIndex > l {
+					return io.ErrUnexpectedEOF
+				}
+				var elementCount int
+				if elementCount != 0 && len(m.States) == 0 {
+					m.States = make([]JobState, 0, elementCount)
+				}
+				for iNdEx < postIndex {
+					var v JobState
+					for shift := uint(0); ; shift += 7 {
+						if shift >= 64 {
+							return ErrIntOverflowEvents
+						}
+						if iNdEx >= l {
+							return io.ErrUnexpectedEOF
+						}
+						b := dAtA[iNdEx]
+						iNdEx++
+						v |= JobState(b&0x7F) << shift
+						if b < 0x80 {
+							break
+						}
+					}
+					m.States = append(m.States, v)
+				}
+			} else {
+				return fmt.Errorf("proto: wrong wireType = %d for field States", wireType)
+			}
+		default:
+			iNdEx = preIndex
+			skippy, err := skipEvents(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if (skippy < 0) || (iNdEx+skippy) < 0 {
+				return ErrInvalidLengthEvents
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			iNdEx += skippy
+		}
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
 func (m *CancelJobSet) Unmarshal(dAtA []byte) error {
 	l := len(dAtA)
 	iNdEx := 0
@@ -10411,6 +10702,75 @@ func (m *CancelJobSet) Unmarshal(dAtA []byte) error {
 			return fmt.Errorf("proto: CancelJobSet: illegal tag %d (wire type %d)", fieldNum, wire)
 		}
 		switch fieldNum {
+		case 1:
+			if wireType == 0 {
+				var v JobState
+				for shift := uint(0); ; shift += 7 {
+					if shift >= 64 {
+						return ErrIntOverflowEvents
+					}
+					if iNdEx >= l {
+						return io.ErrUnexpectedEOF
+					}
+					b := dAtA[iNdEx]
+					iNdEx++
+					v |= JobState(b&0x7F) << shift
+					if b < 0x80 {
+						break
+					}
+				}
+				m.States = append(m.States, v)
+			} else if wireType == 2 {
+				var packedLen int
+				for shift := uint(0); ; shift += 7 {
+					if shift >= 64 {
+						return ErrIntOverflowEvents
+					}
+					if iNdEx >= l {
+						return io.ErrUnexpectedEOF
+					}
+					b := dAtA[iNdEx]
+					iNdEx++
+					packedLen |= int(b&0x7F) << shift
+					if b < 0x80 {
+						break
+					}
+				}
+				if packedLen < 0 {
+					return ErrInvalidLengthEvents
+				}
+				postIndex := iNdEx + packedLen
+				if postIndex < 0 {
+					return ErrInvalidLengthEvents
+				}
+				if postIndex > l {
+					return io.ErrUnexpectedEOF
+				}
+				var elementCount int
+				if elementCount != 0 && len(m.States) == 0 {
+					m.States = make([]JobState, 0, elementCount)
+				}
+				for iNdEx < postIndex {
+					var v JobState
+					for shift := uint(0); ; shift += 7 {
+						if shift >= 64 {
+							return ErrIntOverflowEvents
+						}
+						if iNdEx >= l {
+							return io.ErrUnexpectedEOF
+						}
+						b := dAtA[iNdEx]
+						iNdEx++
+						v |= JobState(b&0x7F) << shift
+						if b < 0x80 {
+							break
+						}
+					}
+					m.States = append(m.States, v)
+				}
+			} else {
+				return fmt.Errorf("proto: wrong wireType = %d for field States", wireType)
+			}
 		default:
 			iNdEx = preIndex
 			skippy, err := skipEvents(dAtA[iNdEx:])
diff --git a/pkg/armadaevents/events.proto b/pkg/armadaevents/events.proto
index a5da0ce1..dbdcd7e1 100644
--- a/pkg/armadaevents/events.proto
+++ b/pkg/armadaevents/events.proto
@@ -232,8 +232,23 @@ message CancelJob {
     Uuid job_id = 1;
 }
 
+
+// Filter to be used when cancelling job sets
+// This allows users to cancel all jobs in a given state for a specific job set
+message JobSetFilter {
+    repeated JobState states = 1;
+}
+
+enum JobState {
+    QUEUED = 0;
+    PENDING = 1;
+    RUNNING = 2;
+}
+
 // Request to cancel all jobs in a job set.
+// Empty set of states means all states
 message CancelJobSet {
+    repeated JobState states = 1;
 }
 
 // Generated by the scheduler in response to CancelJob and CancelJobSet.
diff --git a/pkg/armadaevents/message_types.go b/pkg/armadaevents/message_types.go
deleted file mode 100644
index 083fd018..00000000
--- a/pkg/armadaevents/message_types.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package armadaevents
-
-import "github.com/apache/pulsar-client-go/pulsar"
-
-// PULSAR_MESSAGE_TYPE_PROPERTY is the key of a property included with Pulsar messages.
-// It's value indicates if the message is a control message or a utilisation message.
-//
-// We need this because, we currently submit legacy utilisation messages directly to the log
-// (i.e., not as part of an EventSequence).
-//
-// Hence, consumers can distinguish between control and utilisation messages by inspecting this property.
-// If the property isn't included, the message should be interpreted as a control message.
-const PULSAR_MESSAGE_TYPE_PROPERTY = "type"
-
-const (
-	PULSAR_UTILISATION_MESSAGE = "utilisation"
-	PULSAR_CONTROL_MESSAGE     = "control"
-)
-
-// IsControlMessage returns true if msg is a control message, and false otherwise.
-func IsControlMessage(msg pulsar.Message) bool {
-	if messageType, ok := msg.Properties()[PULSAR_MESSAGE_TYPE_PROPERTY]; ok {
-		return messageType == PULSAR_CONTROL_MESSAGE
-	}
-	return true
-}


Test patch:
diff --git a/e2e/pulsar_test/pulsar_test.go b/e2e/pulsar_test/pulsar_test.go
index 419e4d64..ebdd921d 100644
--- a/e2e/pulsar_test/pulsar_test.go
+++ b/e2e/pulsar_test/pulsar_test.go
@@ -324,7 +324,7 @@ func TestSubmitCancelJobSet(t *testing.T) {
 		}
 
 		// Test that we get submit, cancel job set, and cancelled messages.
-		numEventsExpected := numJobs + 1 + numJobs
+		numEventsExpected := numJobs + numJobs + numJobs
 		sequences, err := receiveJobSetSequences(ctx, consumer, armadaQueueName, req.JobSetId, numEventsExpected, defaultPulsarTimeout)
 		if err != nil {
 			return err
@@ -349,12 +349,14 @@ func TestSubmitCancelJobSet(t *testing.T) {
 				},
 			)
 		}
-		expected.Events = append(
-			expected.Events,
-			&armadaevents.EventSequence_Event{
-				Event: &armadaevents.EventSequence_Event_CancelJobSet{},
-			},
-		)
+		for range res.JobResponseItems {
+			expected.Events = append(
+				expected.Events,
+				&armadaevents.EventSequence_Event{
+					Event: &armadaevents.EventSequence_Event_CancelJob{},
+				},
+			)
+		}
 		for range res.JobResponseItems {
 			expected.Events = append(
 				expected.Events,
diff --git a/internal/armada/repository/job_test.go b/internal/armada/repository/job_test.go
index 4ef1f172..8fa2b006 100644
--- a/internal/armada/repository/job_test.go
+++ b/internal/armada/repository/job_test.go
@@ -14,6 +14,7 @@ import (
 
 	"github.com/armadaproject/armada/internal/armada/configuration"
 	"github.com/armadaproject/armada/internal/common/util"
+	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
 	"github.com/armadaproject/armada/pkg/api"
 )
 
@@ -611,6 +612,26 @@ func TestRetriesOfDeletedJobShouldBeZero(t *testing.T) {
 	})
 }
 
+func TestStoreAndGetPulsarSchedulerJobDetails(t *testing.T) {
+	withRepository(func(r *RedisJobRepository) {
+		details := &schedulerobjects.PulsarSchedulerJobDetails{
+			JobId:  util.NewULID(),
+			Queue:  "testQueue",
+			JobSet: "testJobset",
+		}
+		err := r.StorePulsarSchedulerJobDetails([]*schedulerobjects.PulsarSchedulerJobDetails{details})
+		require.NoError(t, err)
+
+		retrievedDetails, err := r.GetPulsarSchedulerJobDetails(details.JobId)
+		require.NoError(t, err)
+		assert.Equal(t, details, retrievedDetails)
+
+		nonExistantDetails, err := r.GetPulsarSchedulerJobDetails("not a valid details key")
+		require.NoError(t, err)
+		assert.Nil(t, nonExistantDetails)
+	})
+}
+
 func TestUpdateJobs_SingleJobThatExists_ChangesJob(t *testing.T) {
 	withRepository(func(r *RedisJobRepository) {
 		job1 := addTestJobWithClientId(t, r, "queue1", "my-job-1")
diff --git a/internal/armada/server/lease_test.go b/internal/armada/server/lease_test.go
index 18f940ed..32c6d6bc 100644
--- a/internal/armada/server/lease_test.go
+++ b/internal/armada/server/lease_test.go
@@ -205,7 +205,8 @@ func makeAggregatedQueueServerWithTestDoubles(maxRetries uint) (*mockJobReposito
 		fakeQueueRepository,
 		&fakeUsageRepository{},
 		fakeEventStore,
-		fakeSchedulingInfoRepository)
+		fakeSchedulingInfoRepository,
+		fakeExecutorRepository{})
 }
 
 type mockJobRepository struct {
@@ -224,6 +225,18 @@ type mockJobRepository struct {
 	redisError              error
 }
 
+func (repo *mockJobRepository) StorePulsarSchedulerJobDetails(jobDetails []*schedulerobjects.PulsarSchedulerJobDetails) error {
+	return nil
+}
+
+func (repo *mockJobRepository) GetPulsarSchedulerJobDetails(jobIds string) (*schedulerobjects.PulsarSchedulerJobDetails, error) {
+	return nil, nil
+}
+
+func (repo *mockJobRepository) DeletePulsarSchedulerJobDetails(jobId []string) error {
+	return nil
+}
+
 func newMockJobRepository() *mockJobRepository {
 	return &mockJobRepository{
 		jobs:              make(map[string]*api.Job),
@@ -450,3 +463,17 @@ func (repo *fakeSchedulingInfoRepository) GetClusterSchedulingInfo() (map[string
 func (repo *fakeSchedulingInfoRepository) UpdateClusterSchedulingInfo(report *api.ClusterSchedulingInfoReport) error {
 	return nil
 }
+
+type fakeExecutorRepository struct{}
+
+func (f fakeExecutorRepository) GetExecutors(ctx context.Context) ([]*schedulerobjects.Executor, error) {
+	return nil, nil
+}
+
+func (f fakeExecutorRepository) GetLastUpdateTimes(ctx context.Context) (map[string]time.Time, error) {
+	return nil, nil
+}
+
+func (f fakeExecutorRepository) StoreExecutor(ctx context.Context, executor *schedulerobjects.Executor) error {
+	return nil
+}
diff --git a/internal/common/ingest/ingestion_pipeline_test.go b/internal/common/ingest/ingestion_pipeline_test.go
index 1b681179..e2c04d2c 100644
--- a/internal/common/ingest/ingestion_pipeline_test.go
+++ b/internal/common/ingest/ingestion_pipeline_test.go
@@ -190,7 +190,7 @@ func newSimpleConverter(t *testing.T) InstructionConverter[*simpleMessages] {
 	return &simpleConverter{t}
 }
 
-func (s *simpleConverter) Convert(ctx context.Context, msg *EventSequencesWithIds) *simpleMessages {
+func (s *simpleConverter) Convert(_ context.Context, msg *EventSequencesWithIds) *simpleMessages {
 	s.t.Helper()
 	assert.Len(s.t, msg.EventSequences, len(msg.MessageIds))
 	var converted []*simpleMessage
@@ -217,7 +217,7 @@ func newSimpleSink(t *testing.T) *simpleSink {
 	}
 }
 
-func (s *simpleSink) Store(ctx context.Context, msg *simpleMessages) error {
+func (s *simpleSink) Store(_ context.Context, msg *simpleMessages) error {
 	for _, simpleMessage := range msg.msgs {
 		s.simpleMessages[simpleMessage.id] = simpleMessage
 	}
@@ -292,11 +292,12 @@ func testPipeline(consumer pulsar.Consumer, converter InstructionConverter[*simp
 		metricsConfig:          configuration.MetricsConfig{},
 		metrics:                testMetrics,
 		consumer:               consumer,
+		msgFilter:              func(msg pulsar.Message) bool { return true },
 	}
 }
 
-func marshal(t *testing.T, eventSequence *armadaevents.EventSequence) []byte {
-	payload, err := proto.Marshal(succeeded)
+func marshal(t *testing.T, es *armadaevents.EventSequence) []byte {
+	payload, err := proto.Marshal(es)
 	assert.NoError(t, err)
 	return payload
 }
diff --git a/internal/common/pulsarutils/eventsequence_test.go b/internal/common/pulsarutils/eventsequence_test.go
index df774fb9..0613a9f3 100644
--- a/internal/common/pulsarutils/eventsequence_test.go
+++ b/internal/common/pulsarutils/eventsequence_test.go
@@ -9,18 +9,19 @@ import (
 	"github.com/pkg/errors"
 	"github.com/stretchr/testify/assert"
 
+	"github.com/armadaproject/armada/internal/common/schedulers"
 	"github.com/armadaproject/armada/pkg/armadaevents"
 )
 
 func TestPublishSequences_SendAsyncErr(t *testing.T) {
 	producer := &mockProducer{}
-	err := PublishSequences(context.Background(), producer, []*armadaevents.EventSequence{{}})
+	err := PublishSequences(context.Background(), producer, []*armadaevents.EventSequence{{}}, schedulers.Pulsar)
 	assert.NoError(t, err)
 
 	producer = &mockProducer{
 		sendAsyncErr: errors.New("sendAsyncErr"),
 	}
-	err = PublishSequences(context.Background(), producer, []*armadaevents.EventSequence{{}})
+	err = PublishSequences(context.Background(), producer, []*armadaevents.EventSequence{{}}, schedulers.Pulsar)
 	assert.ErrorIs(t, err, producer.sendAsyncErr)
 }
 
@@ -30,7 +31,7 @@ func TestPublishSequences_RespectTimeout(t *testing.T) {
 	}
 	ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond)
 	defer cancel()
-	err := PublishSequences(ctx, producer, []*armadaevents.EventSequence{{}})
+	err := PublishSequences(ctx, producer, []*armadaevents.EventSequence{{}}, schedulers.Pulsar)
 	assert.ErrorIs(t, err, context.DeadlineExceeded)
 }
 
diff --git a/internal/common/pulsarutils/testutil.go b/internal/common/pulsarutils/testutil.go
index ac612793..67c3b127 100644
--- a/internal/common/pulsarutils/testutil.go
+++ b/internal/common/pulsarutils/testutil.go
@@ -16,6 +16,7 @@ type MockPulsarMessage struct {
 	messageId   pulsar.MessageID
 	payload     []byte
 	publishTime time.Time
+	properties  map[string]string
 }
 
 func NewMessageId(id int) pulsar.MessageID {
@@ -56,6 +57,6 @@ func (m MockPulsarMessage) PublishTime() time.Time {
 	return m.publishTime
 }
 
-func (MockPulsarMessage) Properties() map[string]string {
-	return nil
+func (m MockPulsarMessage) Properties() map[string]string {
+	return m.properties
 }
diff --git a/internal/common/schedulers/scheduler_test.go b/internal/common/schedulers/scheduler_test.go
new file mode 100644
index 00000000..361fab36
--- /dev/null
+++ b/internal/common/schedulers/scheduler_test.go
@@ -0,0 +1,45 @@
+package schedulers
+
+import (
+	"testing"
+
+	"github.com/golang/mock/gomock"
+	"github.com/stretchr/testify/assert"
+
+	"github.com/armadaproject/armada/internal/common/mocks"
+)
+
+func TestSchedulerFromMsg(t *testing.T) {
+	tests := map[string]struct {
+		propertyValue     string
+		expectedScheduler Scheduler
+	}{
+		"Legacy": {
+			propertyValue:     LegacySchedulerAttribute,
+			expectedScheduler: Legacy,
+		},
+		"Pulsar": {
+			propertyValue:     PulsarSchedulerAttribute,
+			expectedScheduler: Pulsar,
+		},
+		"All": {
+			propertyValue:     AllSchedulersAttribute,
+			expectedScheduler: All,
+		},
+		"empty": {
+			propertyValue:     "",
+			expectedScheduler: Legacy,
+		},
+	}
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			ctrl := gomock.NewController(t)
+			msg := mocks.NewMockMessage(ctrl)
+			msg.EXPECT().Properties().Return(map[string]string{
+				PropertyName: tc.propertyValue,
+			}).Times(1)
+			result := SchedulerFromMsg(msg)
+			assert.Equal(t, tc.expectedScheduler, result)
+		})
+	}
+}
diff --git a/internal/pulsartest/submit.go b/internal/pulsartest/submit.go
index c658df79..7ce7d06b 100644
--- a/internal/pulsartest/submit.go
+++ b/internal/pulsartest/submit.go
@@ -42,8 +42,7 @@ func (a *App) Submit(path string) error {
 		&pulsar.ProducerMessage{
 			Payload: payload,
 			Properties: map[string]string{
-				requestid.MetadataKey:                     requestId,
-				armadaevents.PULSAR_MESSAGE_TYPE_PROPERTY: armadaevents.PULSAR_CONTROL_MESSAGE,
+				requestid.MetadataKey: requestId,
 			},
 			Key: es.JobSetName,
 		},
diff --git a/internal/scheduler/api_test.go b/internal/scheduler/api_test.go
index a360a80b..32b718ab 100644
--- a/internal/scheduler/api_test.go
+++ b/internal/scheduler/api_test.go
@@ -15,6 +15,7 @@ import (
 	"k8s.io/apimachinery/pkg/util/clock"
 
 	"github.com/armadaproject/armada/internal/common/compress"
+	"github.com/armadaproject/armada/internal/common/mocks"
 	"github.com/armadaproject/armada/internal/common/pulsarutils"
 	"github.com/armadaproject/armada/internal/scheduler/database"
 	schedulermocks "github.com/armadaproject/armada/internal/scheduler/mocks"
@@ -123,9 +124,10 @@ func TestExecutorApi_LeaseJobRuns(t *testing.T) {
 		t.Run(name, func(t *testing.T) {
 			ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 			ctrl := gomock.NewController(t)
-			mockPulsarProducer := schedulermocks.NewMockProducer(ctrl)
+			mockPulsarProducer := mocks.NewMockProducer(ctrl)
 			mockJobRepository := schedulermocks.NewMockJobRepository(ctrl)
 			mockExecutorRepository := schedulermocks.NewMockExecutorRepository(ctrl)
+			mockLegacyExecutorRepository := schedulermocks.NewMockExecutorRepository(ctrl)
 			mockStream := schedulermocks.NewMockExecutorApi_LeaseJobRunsServer(ctrl)
 
 			runIds, err := extractRunIds(tc.request)
@@ -138,6 +140,10 @@ func TestExecutorApi_LeaseJobRuns(t *testing.T) {
 				assert.Equal(t, tc.expectedExecutor, executor)
 				return nil
 			}).Times(1)
+			mockLegacyExecutorRepository.EXPECT().StoreExecutor(ctx, gomock.Any()).DoAndReturn(func(ctx context.Context, executor *schedulerobjects.Executor) error {
+				assert.Equal(t, tc.expectedExecutor, executor)
+				return nil
+			}).Times(1)
 			mockJobRepository.EXPECT().FindInactiveRuns(gomock.Any(), runIds).Return(tc.runsToCancel, nil).Times(1)
 			mockJobRepository.EXPECT().FetchJobRunLeases(gomock.Any(), tc.request.ExecutorId, maxJobsPerCall, runIds).Return(tc.leases, nil).Times(1)
 
@@ -152,6 +158,7 @@ func TestExecutorApi_LeaseJobRuns(t *testing.T) {
 				mockPulsarProducer,
 				mockJobRepository,
 				mockExecutorRepository,
+				mockLegacyExecutorRepository,
 				[]int32{1000, 2000},
 				maxJobsPerCall,
 			)
@@ -201,9 +208,10 @@ func TestExecutorApi_Publish(t *testing.T) {
 		t.Run(name, func(t *testing.T) {
 			ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 			ctrl := gomock.NewController(t)
-			mockPulsarProducer := schedulermocks.NewMockProducer(ctrl)
+			mockPulsarProducer := mocks.NewMockProducer(ctrl)
 			mockJobRepository := schedulermocks.NewMockJobRepository(ctrl)
 			mockExecutorRepository := schedulermocks.NewMockExecutorRepository(ctrl)
+			mockLegacyExecutorRepository := schedulermocks.NewMockExecutorRepository(ctrl)
 
 			// capture all sent messages
 			var capturedEvents []*armadaevents.EventSequence
@@ -222,6 +230,7 @@ func TestExecutorApi_Publish(t *testing.T) {
 				mockPulsarProducer,
 				mockJobRepository,
 				mockExecutorRepository,
+				mockLegacyExecutorRepository,
 				[]int32{1000, 2000},
 				100,
 			)
diff --git a/internal/scheduler/database/redis_executor_repository_test.go b/internal/scheduler/database/redis_executor_repository_test.go
new file mode 100644
index 00000000..6fb48d66
--- /dev/null
+++ b/internal/scheduler/database/redis_executor_repository_test.go
@@ -0,0 +1,86 @@
+package database
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"github.com/go-redis/redis"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"golang.org/x/exp/slices"
+
+	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
+)
+
+func TestRedisExecutorRepository_LoadAndSave(t *testing.T) {
+	t1 := time.Now().UTC().Round(1 * time.Microsecond)
+	tests := map[string]struct {
+		executors []*schedulerobjects.Executor
+	}{
+		"not empty": {
+			executors: []*schedulerobjects.Executor{
+				{
+					Id:   "test-executor-1",
+					Pool: "test-pool-1",
+					Nodes: []*schedulerobjects.Node{
+						{
+							Id:       "test-node-1",
+							LastSeen: t1,
+						},
+					},
+					LastUpdateTime:    t1,
+					UnassignedJobRuns: []string{"run1", "run2"},
+				},
+				{
+					Id:   "test-executor-2",
+					Pool: "test-pool-2",
+					Nodes: []*schedulerobjects.Node{
+						{
+							Id:       "test-node-2",
+							LastSeen: t1,
+						},
+					},
+					LastUpdateTime:    t1,
+					UnassignedJobRuns: []string{"run3", "run4"},
+				},
+			},
+		},
+		"empty": {
+			executors: []*schedulerobjects.Executor{},
+		},
+	}
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			withRedisExecutorRepository(func(repo *RedisExecutorRepository) {
+				ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+				defer cancel()
+				for _, executor := range tc.executors {
+					err := repo.StoreExecutor(ctx, executor)
+					require.NoError(t, err)
+				}
+				retrievedExecutors, err := repo.GetExecutors(ctx)
+				require.NoError(t, err)
+				executorSort := func(a *schedulerobjects.Executor, b *schedulerobjects.Executor) bool {
+					return a.Id > b.Id
+				}
+				slices.SortFunc(retrievedExecutors, executorSort)
+				slices.SortFunc(tc.executors, executorSort)
+				require.Equal(t, len(tc.executors), len(retrievedExecutors))
+				for i, expected := range tc.executors {
+					assert.Equal(t, expected, retrievedExecutors[i])
+				}
+			})
+		})
+	}
+}
+
+func withRedisExecutorRepository(action func(repository *RedisExecutorRepository)) {
+	client := redis.NewClient(&redis.Options{Addr: "localhost:6379", DB: 10})
+	defer client.FlushDB()
+	defer client.Close()
+
+	client.FlushDB()
+	repo := NewRedisExecutorRepository(client, "pulsar")
+	action(repo)
+}
diff --git a/internal/scheduler/publisher_test.go b/internal/scheduler/publisher_test.go
index 200d5581..a524f9e2 100644
--- a/internal/scheduler/publisher_test.go
+++ b/internal/scheduler/publisher_test.go
@@ -15,8 +15,8 @@ import (
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 
+	"github.com/armadaproject/armada/internal/common/mocks"
 	"github.com/armadaproject/armada/internal/common/pulsarutils"
-	schedulermocks "github.com/armadaproject/armada/internal/scheduler/mocks"
 	"github.com/armadaproject/armada/pkg/armadaevents"
 )
 
@@ -92,8 +92,8 @@ func TestPulsarPublisher_TestPublish(t *testing.T) {
 			ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 			defer cancel()
 			ctrl := gomock.NewController(t)
-			mockPulsarClient := schedulermocks.NewMockClient(ctrl)
-			mockPulsarProducer := schedulermocks.NewMockProducer(ctrl)
+			mockPulsarClient := mocks.NewMockClient(ctrl)
+			mockPulsarProducer := mocks.NewMockProducer(ctrl)
 			mockPulsarClient.EXPECT().CreateProducer(gomock.Any()).Return(mockPulsarProducer, nil).Times(1)
 			mockPulsarClient.EXPECT().TopicPartitions(topic).Return(make([]string, numPartitions), nil)
 			numPublished := 0
@@ -167,8 +167,8 @@ func TestPulsarPublisher_TestPublishMarkers(t *testing.T) {
 	for name, tc := range tests {
 		t.Run(name, func(t *testing.T) {
 			ctrl := gomock.NewController(t)
-			mockPulsarClient := schedulermocks.NewMockClient(ctrl)
-			mockPulsarProducer := schedulermocks.NewMockProducer(ctrl)
+			mockPulsarClient := mocks.NewMockClient(ctrl)
+			mockPulsarProducer := mocks.NewMockProducer(ctrl)
 			mockPulsarClient.EXPECT().CreateProducer(gomock.Any()).Return(mockPulsarProducer, nil).Times(1)
 			mockPulsarClient.EXPECT().TopicPartitions(topic).Return(make([]string, numPartitions), nil)
 			numPublished := 0
diff --git a/internal/scheduler/submitcheck_test.go b/internal/scheduler/submitcheck_test.go
new file mode 100644
index 00000000..dd3da87f
--- /dev/null
+++ b/internal/scheduler/submitcheck_test.go
@@ -0,0 +1,175 @@
+package scheduler
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"github.com/golang/mock/gomock"
+	"github.com/google/uuid"
+	"github.com/pingcap/log"
+	"github.com/stretchr/testify/assert"
+	v1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/resource"
+	"k8s.io/apimachinery/pkg/util/clock"
+
+	"github.com/armadaproject/armada/internal/armada/configuration"
+	"github.com/armadaproject/armada/internal/common/util"
+	schedulermocks "github.com/armadaproject/armada/internal/scheduler/mocks"
+	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
+	"github.com/armadaproject/armada/pkg/api"
+)
+
+func TestSubmitChecker_TestCheckApiJobs(t *testing.T) {
+	defaultTimeout := 15 * time.Minute
+	baseTime = time.Now().UTC()
+	expiredTime := baseTime.Add(-defaultTimeout).Add(-1 * time.Second)
+
+	tests := map[string]struct {
+		executorTimout time.Duration
+		config         configuration.SchedulingConfig
+		executors      []*schedulerobjects.Executor
+		jobs           []*api.Job
+		expectPass     bool
+	}{
+		"one job schedules": {
+			executorTimout: defaultTimeout,
+			config:         testSchedulingConfig(),
+			executors:      []*schedulerobjects.Executor{testExecutor(baseTime)},
+			jobs:           []*api.Job{test1CoreCpuJob()},
+			expectPass:     true,
+		},
+		"multiple jobs schedule": {
+			executorTimout: defaultTimeout,
+			config:         testSchedulingConfig(),
+			executors:      []*schedulerobjects.Executor{testExecutor(baseTime)},
+			jobs:           []*api.Job{test1CoreCpuJob(), test1CoreCpuJob()},
+			expectPass:     true,
+		},
+		"first job schedules, second doesn't": {
+			executorTimout: defaultTimeout,
+			config:         testSchedulingConfig(),
+			executors:      []*schedulerobjects.Executor{testExecutor(baseTime)},
+			jobs:           []*api.Job{test1CoreCpuJob(), test100CoreCpuJob()},
+			expectPass:     false,
+		},
+		"no jobs schedule due to resources": {
+			executorTimout: defaultTimeout,
+			config:         testSchedulingConfig(),
+			executors:      []*schedulerobjects.Executor{testExecutor(baseTime)},
+			jobs:           []*api.Job{test100CoreCpuJob()},
+			expectPass:     false,
+		},
+		"no jobs schedule due to selector": {
+			executorTimout: defaultTimeout,
+			config:         testSchedulingConfig(),
+			executors:      []*schedulerobjects.Executor{testExecutor(baseTime)},
+			jobs:           []*api.Job{test1CoreCpuJobWithNodeSelector(map[string]string{"foo": "bar"})},
+			expectPass:     false,
+		},
+		"no jobs schedule due to executor timeout": {
+			executorTimout: defaultTimeout,
+			config:         testSchedulingConfig(),
+			executors:      []*schedulerobjects.Executor{testExecutor(expiredTime)},
+			jobs:           []*api.Job{test1CoreCpuJob()},
+			expectPass:     false,
+		},
+		"multiple executors, 1 expired": {
+			executorTimout: defaultTimeout,
+			config:         testSchedulingConfig(),
+			executors:      []*schedulerobjects.Executor{testExecutor(expiredTime), testExecutor(baseTime)},
+			jobs:           []*api.Job{test1CoreCpuJob()},
+			expectPass:     true,
+		},
+		"gang job all jobs fit": {
+			executorTimout: defaultTimeout,
+			config:         testSchedulingConfig(),
+			executors:      []*schedulerobjects.Executor{testExecutor(baseTime)},
+			jobs:           testNJobGang(5),
+			expectPass:     true,
+		},
+		"gang job all jobs don't fit": {
+			executorTimout: defaultTimeout,
+			config:         testSchedulingConfig(),
+			executors:      []*schedulerobjects.Executor{testExecutor(baseTime)},
+			jobs:           testNJobGang(100),
+			expectPass:     false,
+		},
+	}
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+			defer cancel()
+
+			ctrl := gomock.NewController(t)
+			mockExecutorRepo := schedulermocks.NewMockExecutorRepository(ctrl)
+			mockExecutorRepo.EXPECT().GetExecutors(ctx).Return(tc.executors, nil).AnyTimes()
+			fakeClock := clock.NewFakeClock(baseTime)
+			submitCheck := NewSubmitChecker(tc.executorTimout, tc.config, mockExecutorRepo)
+			submitCheck.clock = fakeClock
+			submitCheck.updateExecutors(ctx)
+			result, msg := submitCheck.CheckApiJobs(tc.jobs)
+			assert.Equal(t, tc.expectPass, result)
+			if !tc.expectPass {
+				assert.NotEqual(t, "", msg)
+			}
+			log.Info(msg)
+		})
+	}
+}
+
+func test1CoreCpuJob() *api.Job {
+	return &api.Job{
+		Id: util.NewULID(),
+		PodSpec: &v1.PodSpec{
+			Containers: []v1.Container{
+				{
+					Resources: v1.ResourceRequirements{
+						Limits: map[v1.ResourceName]resource.Quantity{
+							"cpu": resource.MustParse("1"),
+						},
+						Requests: map[v1.ResourceName]resource.Quantity{
+							"cpu": resource.MustParse("1"),
+						},
+					},
+				},
+			},
+		},
+	}
+}
+
+func testNJobGang(n int) []*api.Job {
+	gangId := uuid.NewString()
+	gang := make([]*api.Job, n)
+	for i := 0; i < n; i++ {
+		job := test1CoreCpuJob()
+		job.Annotations = map[string]string{testGangIdAnnotation: gangId}
+		gang[i] = job
+	}
+	return gang
+}
+
+func test100CoreCpuJob() *api.Job {
+	job := test1CoreCpuJob()
+	hundredCores := map[v1.ResourceName]resource.Quantity{
+		"cpu": resource.MustParse("100"),
+	}
+	job.PodSpec.Containers[0].Resources.Limits = hundredCores
+	job.PodSpec.Containers[0].Resources.Requests = hundredCores
+	return job
+}
+
+func test1CoreCpuJobWithNodeSelector(selector map[string]string) *api.Job {
+	job := test1CoreCpuJob()
+	job.PodSpec.NodeSelector = selector
+	return job
+}
+
+func testExecutor(lastUpdateTime time.Time) *schedulerobjects.Executor {
+	return &schedulerobjects.Executor{
+		Id:             uuid.NewString(),
+		Pool:           "cpu",
+		LastUpdateTime: lastUpdateTime,
+		Nodes:          testNodeItems1(),
+	}
+}
diff --git a/internal/scheduleringester/instructions_test.go b/internal/scheduleringester/instructions_test.go
index cbff5df9..bd8f2381 100644
--- a/internal/scheduleringester/instructions_test.go
+++ b/internal/scheduleringester/instructions_test.go
@@ -27,7 +27,6 @@ var (
 func TestConvertSequence(t *testing.T) {
 	tests := map[string]struct {
 		events   []*armadaevents.EventSequence_Event
-		filter   func(event *armadaevents.EventSequence_Event) bool
 		expected []DbOperation
 	}{
 		"submit": {
@@ -171,13 +170,6 @@ func TestConvertSequence(t *testing.T) {
 				MarkJobsSucceeded{f.JobIdString: true},
 			},
 		},
-		"filtered events": {
-			events: []*armadaevents.EventSequence_Event{f.JobSetCancelRequested, f.Running, f.JobSucceeded},
-			filter: func(event *armadaevents.EventSequence_Event) bool {
-				return event.GetJobRunRunning() != nil
-			},
-			expected: []DbOperation{MarkRunsRunning{f.RunIdUuid: true}},
-		},
 		"ignored events": {
 			events: []*armadaevents.EventSequence_Event{f.Running, f.JobPreempted, f.JobSucceeded},
 			expected: []DbOperation{
@@ -189,12 +181,7 @@ func TestConvertSequence(t *testing.T) {
 
 	for name, tc := range tests {
 		t.Run(name, func(t *testing.T) {
-			if tc.filter == nil {
-				tc.filter = func(event *armadaevents.EventSequence_Event) bool {
-					return true
-				}
-			}
-			converter := InstructionConverter{m, tc.filter, f.PriorityClasses, compressor}
+			converter := InstructionConverter{m, f.PriorityClasses, compressor}
 			es := f.NewEventSequence(tc.events...)
 			results := converter.convertSequence(es)
 			assertOperationsEqual(t, tc.expected, results)